Two reported gaps:
- a scripted PROVIDE LOCAL INFORMATION expectation answered with an empty
TR - the PLI dictionary (and the host clock for the date/time qualifier)
was silently inert during runs while the poll path used it all along, so
an applet waiting for its data stalled behind a passing expectation. A
POLL INTERVAL expectation likewise never echoed the Duration (6.8.4).
testscript.build_tr() gained `answer_tlvs` (inserted after the Result per
the 6.8.0 order, before text/item/raw) and the shared
_test_terminal_response() wires the expect step, the proactive-drain
action and the run cleanup to it; the script's `raw` overrides, and a
non-success result gets no data object.
- a GP INSTALL/LOAD/DELETE step always failed the default exact-9000 check
although the envelope SW 91XX is the successful answer (the re-announced
SET UP MENU; XX is the pending command's length, never pinnable).
Documented rather than loosening the default: a new "Driving a RAM install
from a script" subsection in docs/test-scripts.md (the 9??? mask + por
any, the per-step pending rule - a proactive-drain/expect after every
91XX step), a sentence at the sw-default bullet, and an scp80
default-pinning test.
Docs: api.md's respond clause, help EN/RU's TR Config note.
Tests: 839 frontend + 812 Python; sw cache simple-v406.
The three nits from the review of the rename/docs commits:
- the PWA comment above TEST_ACTION_KINDS still claimed the final SCP80
counter is written back - the server persists every consumed counter into
the preset and the snapshot carries the latest values (display only);
- test-scripts.md's `por` paragraph regained the RM raw fallback the rewrite
dropped: a remote-management reply that cannot be the compact structure
(its command count exceeds the command script that was sent, or the data
is too short for TS 102 226 5.1.2 Table 5.1) is raw too;
- the RU help label reads "Очистка очереди проактивных команд" (the mixed
Latin "proactive" is gone; the rest of the RU help says "проактивных
команд"). sw cache simple-v405.
Docs/comment only - tests unchanged and green: 839 frontend + 804 Python.
- the actions table still named the drain kind `proactive` (the rename swept
the prose but missed the table row);
- the `por` check paragraph carried the pre-3.21.0 rule (the command-count
bound): the response form follows the command's TAR and a short reply is
raw too - now cross-referenced to "The three status words";
- the event action's `src`: the editor writes the event's own source
(network for MT call / IMS registration), a hand-written step without
`src` is sent as the terminal;
- the format line notes the kind was `proactive` until 3.22.1;
- the cleanup section notes that each suite member ends the same way, the
"no actions" note names the ok drain, and the polling limitation mentions
proactive-drain.
Docs only.
The kind string `proactive` read like an expectation of a proactive command
(`expect` is the step type for that) although the action consumes and answers
them. Renamed everywhere:
- ACTION_KINDS / TEST_ACTION_KINDS (drift-tested), the normaliser branch and
its error strings, the dispatch and the helper names (_test_run_drain,
_drain_require_match/_label);
- the report label is DRAIN and the PWA summary reads "DRAIN x5 require ...
TR ok";
- the editor/collect/summary/problem branches, docs/test-scripts.md
(Proactive drain), docs/api.md, help EN/RU ("Очистка очереди proactive");
- the tests (TestRunnerProactiveDrain, the normalisation/data-check tests).
No stored scripts used the old kind (unpublished series), so there is no
alias and no migration. sw cache simple-v404.
Tests: 839 frontend + 804 Python green.
Second review round on the unpushed v3.20.0-v3.22.0 batch:
- install parameters: a zero memory quota (nv_quota: 0 - the vendor
reference form's C7 02 0000 C8 02 0000) is composed instead of being
dropped as "unset" (only None/blank leaves the TLV out); a truncated EF
length in stk_params is refused instead of read as an empty EF.
- test suites: a suite whose member script is gone is repairable - the
update endpoint accepts pruning a dangling reference (a removal of a
live script stays refused; the PWA's row Delete prunes it), and
reconcile_scripts_to_suites() lists an orphan even when its suite_id
write is refused by the validator, so a stored script that no longer
validates stays visible in the editor and fails at run start with the
message instead of disappearing.
- engine/PWA: the proactive action refuses a check.data (its result data
is the drained-command list - the check was silently ignored); the
vestigial _testScriptDraft is gone; berLenStr emits the 2/3-byte BER
long form (a >255-byte C9 composes correctly).
- docs: api.md (the stop cleanup answers ok, never an automatic cancel;
the server persists the counters - the PWA does not write them back; the
suite bundle import always merges the scripts; the quota-0 note) and
test-scripts.md (the event action covers all modelled events; the
direct-create path needs a suite_id; the format history); help EN/RU
counter wording.
- tests: the zero quota / truncated EF, the prune path, the invalid-script
listing, the proactive data-check refusal, berLenStr and the >255-byte
C9. Tests: 839 frontend + 804 Python; sw cache simple-v403.
The suite/script ownership invariant could break silently: a suites import
with mode "replace" wiped the suites while their scripts stayed behind
(suite_id pointing at gone suites - invisible in the UI), a scripts import
with mode "replace" left every suite's member list dangling, and a script
whose suite_id disagreed with its listing suite stayed inconsistent.
- server: reconcile_scripts_to_suites() repairs the invariant - a script a
suite lists adopts that suite (the member list is authoritative), a script
no suite lists joins "Imported scripts"; migrate_scripts_to_suites()
delegates to it (the startup migration is the same pass) and the suites
import runs it after loading, so a "replace" adopts the wiped suites'
scripts instead of losing them.
- the scripts import refuses mode "replace" while any suite still lists
scripts (400 naming the dangling references; merge is the normal path).
- nits from the review: a legacy numeric `event` is no longer parsed as hex
in the step summary/error check (0x0A displayed as 0x10), and an integral
float memory quota (a JSON client's 200.0) is accepted.
- tests: the reconciliation (missing suite, listed-but-mismatched,
idempotent), the scripts-replace guard (refused/allowed), the
suites-replace adoption, the float quota, the numeric-event summary.
Tests: 838 frontend + 800 Python; sw cache simple-v402.
The test-script event action edited its fields as a JSON textarea and only
accepted the four events events.py builds; every other event needed the raw
envelope action with hand-copied hex. It now renders the same EVENT_FORMS
form the Phone tab's send dialog uses, for every modelled event (29; no
release gating - a script may target another card).
- PWA: the field renderer/collector is shared (eventFormFieldsHtml /
eventFormValues / eventFormPatternError / eventFormSrc / eventFieldGroupsUpdate);
the Phone tab keeps its ids, its release gating and its onchange updaters,
the step editor uses test-f-ev- ids, the generic group updater and a live
Event data (hex) preview with the one-ENVELOPE check. The event select
lists all EVENT_FORMS events (the numeric keys are decimal strings -
parseInt(k, 10)); the action stores the collected fields, the built data
and the event's source (fixed cfg.src or the cfg.srcField input - the
action never carried src before, so MT call / IMS registration / slices
could not be network-sourced). The JSON textarea is gone.
- engine: the `event` action keeps `fields` validation for the four
server-built events (a stray data hex is ignored) and accepts the
form-built `data` for every other event - hex-validated and checked
against events.EVENT_DATA_MAX (244, the one-ENVELOPE budget; server.py's
_EVENT_DATA_MAX now reads it from events.py). The runner builds the four
at run time (fresh host clock) and sends the stored data for the rest.
- tests: the engine's data path (accepted / missing / malformed / oversized,
the four still fields-built), the runner sending the data (fake scc), the
editor's collect/prefill/patterns/source (Node), the shared helpers.
Docs: test-scripts.md, api.md, help EN/RU. Tests: 838 frontend +
797 Python; sw cache simple-v401.
A suite is the root object: every script belongs to exactly one suite, the
suite holds the ordered member list by reference - the optional setup first,
the members, the optional teardown last - and runs them in one card session,
without resets and without resume.
- test_suites.py: the suite store (test_suites.json, --test-suites), validated
and atomic like the script store: roles normalised (setup first, teardown
last, at most one of each, a script appears once per suite), per-member
on_fail stop/continue and the suite-level require_adm.
- test_scripts.py schema v2: suite_id/require_adm; list_by_suite()/orphans();
the import reports the id_map a bundle import remaps with. v3.21.0 scripts
without a suite are attached once to an auto-created "Imported scripts"
suite (migrate_scripts_to_suites, called from __main__).
- API: /api/test/suites (+ update|delete|import - delete refused while
non-empty; the bundle import imports the scripts first, remaps the suite
references and skips unresolvable ones); the scripts endpoints gain
suite_id/require_adm plus move|copy (delete detaches from the suite first;
a suite update refuses a membership change).
- runner: /api/test/run {suite_id} resolves the members and the preset, runs
the ADM gate, then setup -> members (per-member on_fail) -> teardown. The
teardown always runs on a stop or a failure (skipped only when the card is
gone; it clears the stop flag for its duration); a card reset stops the
suite, skips the remaining members and is flagged. Members' live steps land
in the shared snapshot, their records (status/steps/note/log excerpt/
counters) in _TEST_RUN['suite']; the summary carries the counts, wall time,
preset, the SCP80 counters before/after per keyset and the session flags.
_test_log buffers the semantic lines (bounded) for the report's per-member
excerpts.
- ADM gate (_ensure_adm): a suite/script require_adm verifies the matched
preset's key once per card session (the /api/verify-adm badge sets the same
latch); a failure refuses the start, stamps the session (no automatic
retry) and needs a manual verify.
- PWA: the Test script pill becomes four views - the suite list (default),
the suite editor (the script list: role badges, on_fail, up/down, per-script
Run/Edit/Move/Copy/Delete, Add script with the templates), the script editor
(the step list; a single run shows its result under the steps and is not
preserved) and the suite report (per-member blocks with the step table,
notes and run log; summary; export JSON/Markdown). Suite bundles export/
import; the legacy script export imports into "Imported scripts". EN/RU.
- tests: the suite store/migration, the runner orchestration (order, on_fail,
setup failure, stop, card reset, teardown, per-member logs) and the ADM gate
(Python); the suite HTTP paths; the PWA report/Markdown/wiring (Node).
Tests: 833 frontend + 794 Python; sw cache simple-v397.
Five reported issues, plus the action that makes an explicit drain possible:
- on_fail is honoured by every step failure: an expectation with no pending
command (or a FETCH that returns nothing) fails as a Command/Fetch check at
the step's level instead of raising past it; the default still stops the
run, on_fail "warning" continues (the tolerant consume-if-pending form).
- the run cleanup answers ok (0x00) and keeps draining while the TR answers
91XX (bounded); a cancel is only sent when a script asks for it - it tells
the card the user aborted the session and pushes assertive applets onto an
error path.
- new `proactive` action: STATUS rounds consume every announced command
(respond/first TERMINAL RESPONSE results, attempts/interval_ms bound) until
the card answers 9000; an optional require asserts at least one drained
command (an empty drain fails it), the final SW is the step's SW check and
a still-pending command is handed to the next step. PWA editor + summary +
validation, EN/RU help, docs, action-kind drift test.
- the PoR response form follows the command's TAR: _tar_is_rm (preset role
entries + the TS 101 220 Annex D ISD/RFM allocations) decides; a non-RM
TAR's reply is always raw (TS 102 226 4) - no fabricated status word even
when the bytes happen to parse (01 00 0B 91 ... used to read as SW 000B).
- a secured data too short for the compact structure (a 2-byte applet reply)
is recovered by _por_header_parse (the decode minus the compact parse; a
malformed packet or a failed RC/CC/DS check still yields no por) and
reported raw, so short applet replies are assertable via por.data.
Docs: test-scripts.md, api.md, help EN/RU, repo AGENTS. sw cache simple-v396.
Tests: 825 frontend + 755 Python green.
Two reported SCP80/RAM issues, both confirmed against the specs:
- docs/api.md called `stk_params` a "Hex CA TLV", but TS 102 226 8.2.1.3.2.1
puts the CA *inside* the System Specific Parameters EF ('EF'), and the PWA
builds `EF <len> CA <len> ...` (the SIM access domain leads the CA value as
`01 <ad>` / `00`). A client following the doc sent a CA at the wrong level.
- the RAM composition appended the STK part after a caller-composed
C9+EF(quotas) prefix, producing TWO EF objects. The card read the first
(the quotas) and ignored the CA: the install was accepted (por_ok) but the
toolkit parameters were never registered, so no STK menu appeared (live
2026-10-05). Per GPD_SPE_013 v1.1 Table 6-5 (pySim's
gen_install_parameters) the quotas (C7/C8) and the CA share ONE EF; an
EA-form STK part is a sibling of EF (the reference TCA loader's
`C9 00 EF 00 EA ...`).
- server: `_compose_install_params()` (used by `_cap_install_apdu` ->
`_cap_apdu_sequence`, hence /api/ram-install, /api/ram-install-app,
/api/scp81/gen-install and the PWA's install script template) enforces the
rule: a caller-composed EF plus an EF-form `stk_params`, a bare CA, a
malformed EF and quotas next to a non-empty `install_params` are refused
(400 / `{"ok": false}` with the fix named); with an empty `install_params`
the quotas and an EF-form CA are composed into one EF
(`C900 EF{C7?,C8?,CA}`), an EA part stays a sibling.
- the documented `nv_quota`/`volatile_quota` fields are now real (they were
named in the docs but implemented nowhere): integers composing C8/C7.
- PWA: `composeRamInstallParams()` composes the same single EF itself (a
CA-form STK field takes the quotas into its EF, the merged EF travels in
`stk_params`), the RAM install bodies send it, the placeholder shows it
with a note when the quotas moved into the STK EF, and a refused request
(no steps) reports the error instead of "FAILED at step: undefined".
- docs/api.md: the corrected `stk_params` description with both accepted
shapes, the composition rule paragraph with the refusal cases, the fixed
example (`C90000` -> `C900`), and the /api/ram-install-app and
/api/scp81/gen-install cross-references.
- tests: composition unit tests (merge/refusals/EA-sibling) and the
ram-install-app 400 paths (Python); `ramQuotaTlvs`/`composeRamInstallParams`/
`ramInstallParamsCompose` pins incl. the CA merge (Node).
Tests: 824 frontend + 740 Python; sw cache simple-v395.
The OTA CPI information element is a request field (default true) on
/api/send-ota and the RAM family but was only mentioned in passing:
- /api/send-ota: an "SMS framing - includeCpi" paragraph (the CPI IE marker
per TS 31.115 4.2/4.3; 137 vs 140 octets in a single SM, first concatenated
segment 132 vs 134; the RAM/ISD operations send with it, a concatenated
command carries it in its first SM; what false does) and
"includeCpi": true in the request-body example;
- /api/ram-install and /api/ram-install-app: field-table rows;
- /api/cap-compat: the "as /api/ram-install" field list;
- /api/tar-probe: a sentence.
Docs only - no version/sw bump.
The scripted-SELECT note cited TS 102 226 7.2 alone although the paragraph
right above lists both B00000 and B00001: 7.2 is the UICC shared FS RFM
application (implicitly selected in MF), while an ADF RFM application starts
in its ADF (7.3). Also says the script has no *use* for an FCI rather than
nowhere to put it.
Two boundary flaws found reviewing v3.20.0:
- /api/send-ota decoded the PoR with the request's `cntr`, not the counter
the packet was actually built with. With the new seeding an omitted
counter is the normal case, so a warning ENVELOPE's verdict - notably the
live card's cntr_low PoR - could fail its RC/CC check and be lost (the
success and the failure path are both fixed). Tests pin the decode counter
on both paths; the failure path also echoes the effective `cntr`.
- the PWA's effective counter (preview / verify / the older-server fallback)
fell back to the preset's first keyset and then 1, while the server's empty
seed builds counter 0 (no preset / kvn 0 / an unknown keyset). A manual
form or a keyless packet then produced a spurious "Verify vs pySim"
MISMATCH and a preview that differed from the delivered packet.
spPresetCounterValue()/spCntrEffective() now mirror _preset_counter_seed()
exactly: the packet's matched keyset, else 0.
- the `OTA SEND: counter not tracked` line reflects the effective counter (a
seeded value is logged too); docs/api.md records that the seed uses the
packet's keyset whatever SPI1.b5b4 says.
Tests: 823 frontend + 737 Python; sw cache simple-v394.
The counter an operation consumed was re-entered by the PWA on every send
(the form field was filled from the preset and posted back), so a stale tab
could send a value the card rejects (cntr_low), and typing in the field
silently wrote the preset. Now the store is the single source of truth and
the form field is a per-send override (Option B).
- _preset_counter_seed: an explicit `cntr` in the request wins (a hand send,
the counter probe or a pre-built packet may deliberately pick one);
otherwise the preset store's value for the packet's keyset. Wired into
/api/send-ota (the APDU path; the pre-built `sp` path keeps the caller's
bookkeeping value - the packet carries its own counter), /api/ram-install,
/api/ram-install-app, /api/cap-compat, /api/tar-probe and /api/sp-verify
(the reference must use the counter the JS packet was built with).
- the send-ota response echoes the effective `cntr` (a caller that omitted
one learns what was used) and the OTA SEND log line prints it.
- persistence is unchanged and monotonic (presets.set_counter refuses
backwards writes), pinned by tests: an explicit lower counter is used for
the packet but never lowers the store, a higher accepted one is saved.
- PWA: the Secured packet counter field starts empty with the preset's next
value as its placeholder (auto: <cntr>); typing affects that send only and
is never written to the preset (spCntrSyncPreset is gone), spParams /
getRamSpParams omit an empty counter, the RAM/send flows sync only the
local preset copy (forward-only) instead of filling the field, the +/-
buttons start from the effective value, and the preview/verify packet is
built from the effective counter (genSp). Placeholder translated EN/RU.
- the runner and /api/counter-probe keep their existing seeding (the probe
already starts at the failed value and walks above it).
- docs: api.md's optional-`cntr` contract + seeding notes on every endpoint,
help EN/RU 3.1. Tests: 822 frontend + 734 Python; sw cache simple-v393.
The alfa-dsa RFM script work showed a card refusing the P2='00' (FCI) form
of a SELECT inside a secured command script with 6A86, while P2='0C' (no
response data) executes - and a script has nowhere to put an FCI anyway.
Worksheet 1 gains the note (TS 102 226 7.2: the RFM application starts in
MF); the repo AGENTS records the observation. Docs only - no code, no
version or sw change.
The alfa-dsa control-commands run showed the error case: an applet-level
refusal (envelope 6200 for an unknown command) carries no PoR at all, so
a por expectation on such a step can never match. The authoring guide's
status-words section now states that a 62XX/63XX envelope does not
guarantee a response packet - other warning paths still deliver one (the
low-counter case arrives via SEND SHORT MESSAGE) - and that a step
expected to be refused should leave por at its "any" default or assert
"none"; help EN/RU notes the same. docs/test-scripts.md only, no code
change; sw cache simple-v392 (help files are precached).
The script author read por.sw as the envelope SW, so a por.sw expectation
on an applet's own TAR failed with (none).
- docs/test-scripts.md: a 'The three status words (envelope SW / PoR
status / R-APDU SW)' table in the authoring section - where each one
comes from, the script field and the report row - plus a cross-reference
from the por check paragraph; help EN/RU 8.9 points at it.
- a failed por.sw check on a response_type: 'raw' decode carries the check
detail 'the response is application data - an applet's own TAR has no
R-APDU SW; assert it with data' (the PWA renders check details).
- the runner's log prints APP DATA=<hex> (no R-APDU SW) for application
responses instead of the misleading R-APDU SW=(none) data=...; the
compact/scripting lines are unchanged.
- the step editor's custom-PoR block gets a one-line hint about the same.
Tests: 821 frontend + 730 Python; sw cache simple-v391.
pySim's CompactRemoteResp was applied to every response packet, so a
third-party applet's own TAR response parsed as count=128/SW=0110/data=''
(live 2026-10-04: the alfa-dsa applet's `80 01 10`) - a fabricated status
word and a lost response.
_decode_por now takes the plaintext command script's length (cmd_len): the
compact structure's command count cannot exceed it (TS 102 226 5.1.2
Table 5.1 - each command needs at least one byte), so a larger count is
classified `response_type: 'raw'`, the secured data becomes
`decoded.last_response_data` (a por.data check matches it) and no SW is
decoded; `secured_data` is exposed in every case.
Callers pass cmd_len where they know the plaintext: the test runner's scp80
step (None for a pre-built packet; carried in ctx['last_scp80'] for the
submit-PoR path), /api/send-ota (both paths), the RAM step sender and the
counter probe; a caller without it keeps the previous parse.
Docs: test-scripts.md and api.md describe the raw fallback.
Tests: 821 frontend + 729 Python; sw cache simple-v390.
An audit of docs/test-scripts.md against the engine (testscript.py,
events.py, the runner) found a handful of doc-level gaps, now fixed:
- the scp80 param defaults (tar = the preset's ISD role TAR, spi1 = that
TAR's MSL, spi2 = 01, kvn = the first keyset);
- the counter rule as implemented: the run advances/persists the counter
after every answered scp80 step when SPI1 requests a counter check
(refused packets move the local value forward too - it only moves up);
- respond is optional (an omitted object answers ok/0x00);
- the run-log section: the exact READ BINARY/READ RECORD/UPDATE BINARY,
SCP80 WRAPPED=/SP= and 'TEST-RUN step N failed:' lines;
- Known limitations: an expanded-format 61xx has no in-packet GET RESPONSE
(TS 102 226 5.2.1.1) - send the GET RESPONSE as a follow-up scp80 step;
- sms check da: a leading + and whitespace are ignored. The engine now
drops the + (the TPDU carries the international flag in the dialling
type, so '+79...' could otherwise never match the parsed digits);
docs/api.md and the local AGENTS note it.
Tests: 821 frontend + 726 Python.
Review of 7250dbd:
- the step editor's menu-select form gains a Match case select
(Case-insensitive (default) / Case-sensitive); the step summary flags a
case-sensitive step with (case), the saved params carry
case_sensitive: true only when chosen, and the docs say the editor
authors the flag.
- docs/api.md: fix the 'with an optionally' grammar.
- the Variant-2 menu test's comment claimed a contains match while it is an
exact one; a real contains leg is added ('ыбор' -> item 128).
- timeless rationale for the case-insensitive default; the format note now
dates the change.
sw cache simple-v389 (index.html/help changed).
Tests: 821 frontend + 726 Python.
No test scripts have been authored yet, so the format is still free to
change - the menu-select text match is now case-insensitive by default
(menu labels are UI text; case_sensitive: true still forces an exact-case
match, and a miss on it names the case-insensitive candidates).
Verification against the live card's own bytes (captured 2026-10-04):
- the Annex A Variant 2 Cyrillic menu (ids 128..130) resolves by text and
the case behaviour above is pinned ('кофе' -> item 130);
- the applet's SEND SHORT MESSAGE (alpha 'Отправка...', SMS-SUBMIT to
+79332505884) pins the new alpha and sms checks and the run-log line.
docs/help/api updated; sw cache simple-v388 (help pages changed).
Tests: 819 frontend + 726 Python.
The test scripts get the pieces an applet test needed:
- scp80 check.por accepts an object {status?, sw?, data?} asserting the
decoded PoR of this very exchange (inline or via SEND SHORT MESSAGE);
the evaluation is shared with the expect-step por check
(_por_check_results). The editor's PoR select gains 'custom' with the
three fields (an empty object refuses on save).
- a new alpha content check reads the command's Alpha identifier (05/85,
Annex A coded) - the text check stays bound to the 8D Text string, so a
command carrying both keeps them distinct.
- a new sms content check asserts a SEND SHORT MESSAGE's submit TPDU:
DA / PID / DCS / UDL / UD (the parsed fields are logged and reported;
an applet's own SMS, not the OTA PoR the por check decodes).
- scp80.params.format wraps the C-APDU in the TS 102 226 5.2.1 Command
Scripting template (expanded = AA..., expanded-ae = AE 80...00 00); the
log shows WRAPPED=, the report C-APDU=<effective> format=<fmt>.
- docs: Worksheet 1 addresses an RFM TAR (B00000/B00001/B00010 compact,
B00120/B00140/B00130 expanded) with the own-TAR and MSL/spi1 notes, and
file-write is documented as a direct APDU that does not fire
EVENT_REMOTE_FILE_UPDATE.
Tests: 819 frontend + 724 Python; sw cache simple-v387.
Verifying a script from the log was impossible: the runner wrote no card
lines, SCP80's plaintext C-APDU / secured packet / PoR landed only in the
report, and a menu selection logged nothing (the interactive path showed just
"STATUS poll (menu-select 9000)" / "STATUS -> 9000").
Every step now writes a `TEST-RUN step N:` line family to stderr, request
before response:
- generic actions: the APDU/raw event data sent and the SW/RESP received
(`APDU TX=...`, `ENVELOPE(Event Download) ... data=...`, `READ/UPDATE ... -> SW=...`);
- SCP80: `C-APDU=` (or `SP=`), `SECURED=<hex> (N B) TAR=... SPI1=... SPI2=... CNTR=...`,
the response SW, and the PoR - `PoR[inline] status=... TAR=... CNTR=... raw=...`
plus `R-APDU SW=... data=...` (with the bad-format note);
- expectations: `FETCH=<apdu> -> SW=... <raw>`, `CMD 0x.. <NAME> qual=..`,
`TR=<apdu> -> SW=...`, and `PoR[sms-submit] ...` + `R-APDU` for the SEND
SHORT MESSAGE transport;
- status: `STATUS 1/N -> <sw>` per attempt;
- menu selection: `MENU-SELECT ENVELOPE=<apdu> item=<id> ('<text>') -> SW=...`
(both the runner action and /api/menu-select; the old STATUS lines stay as
context).
The shared secured-packet sender logs each segment's answer
(`OTA SEND: ENVELOPE i/N -> <SW>`), the scp80 step's report `sent` now carries
the C-APDU/SP hex, and `--apdu-trace` remains the raw transport view.
help EN/RU and docs/test-scripts.md gained the "reading the run log" note.
Tests: 816 frontend + 718 Python; sw cache simple-v385.
The events without a semantic builder (MT call, IMS registration, slices, ...)
are network-sourced, but the raw `envelope` action had no `src` override - the
only way to express them was the semantic set. `envelope` now accepts
`src` (82 terminal / 83 network) exactly like the `event` action; the step
editor exposes it and the authoring guide gets the row.
Also: the guide marks `por` as SCP80-only (the normaliser refuses it
elsewhere) and the semantic-events section names the shared src override.
Tests: 813 frontend + 717 Python; sw cache simple-v382.
The other-applet batch: a test script can send the common event downloads
semantically instead of raw hex.
- events.py: server-side mirrors of the PWA's event forms for Location status
(0x03), Access technology change (0x0B), Network rejection (0x12 - the
LA/RA/TA object selected by the registration group, the extended EMM cause
and the extended information object) and Data connection status change
(0x1D - status/type/TI plus the optional cause, host-clock 8.39 date-time,
location information, access technology, APN and PDP/PDN/PDU type). Fields
are validated before the run (a bad value is a script error, not a step
failure); `src` overrides the device-identities source (default terminal,
matching the Phone forms).
- The engine gains the `event` action kind; the runner builds and sends it.
The PWA's action-kind list stays in sync (drift-guarded - the guard itself
caught its own regex bug on the first run).
- Byte parity: the vectors are generated from the PWA's own builders and
pinned in tests/test_events.py; the frontend suites assert the same values.
PLMN and SCTS helpers are mirrored too.
- The Phone tab's event form shows the built "Event data (hex)" (updated as
the fields change), so every event without a builder can be used through
the raw `envelope` action.
- The test-step editor offers the event select + a JSON fields textarea;
malformed JSON refuses the save with a clear message.
- docs/test-scripts.md (the semantic-event table, an event example, the
format stamp), docs/api.md and help EN/RU updated.
Tests: 812 frontend + 717 Python; sw cache simple-v381.
The applet-testing batch agreed after v3.15/3.16:
- menu-select by text: the step names the item by `text` (+ `mode`
exact/contains, `case_sensitive`) instead of the install-dependent id
(both or neither is refused); the runner resolves it against the card's
cached menu and raises a step error naming the available items when nothing
(or several items) matches. The cache is refreshed on every SET UP MENU
fetch - the proactive chain and the expect step - so an applet install (new
items/ids after its REFRESH) no longer leaves a stale menu behind. The
step editor offers the id or the text + mode and the client-side pre-check
bounds them.
- TERMINAL RESPONSE object order: both builders emitted the Item identifier
before the Result (and the scripted builder its Text string before it too);
TS 102 223 6.8.0 mandates Command details, Device identities, Result,
Duration, Text string, Item identifier. `testscript.build_tr` and
`_menu_send_response` now follow it; `respond.raw` is documented as
appended after the standard objects, and the exact-byte tests pin the order.
- Text codings: the scripted response codes the GSM default alphabet via the
GSM 03.38 codec ('00' packed, '04' 8-bit) instead of latin-1, keeps '08'
UCS2 and sends the spec's null text string for an empty answer; the packing
helper is shared with the interactive path (`testscript.pack_gsm7`). A
UCS2 GET INPUT answer is documented and tested.
- docs/test-scripts.md gained the handoff brief for the applet agent: the
value-collection table, fill-in worksheets for the two applet scenarios, a
menu-by-text example, the validation checklist and a format stamp; help
EN/RU and docs/api.md updated.
Tests: 809 frontend + 705 Python; sw cache simple-v380.
The scripts lived in the browser's localStorage although only the local
server can run them. The new test_scripts.py store
(~/.pysim-simple-server/test_scripts.json, --test-scripts PATH) sits next to
the card presets: stable uuids and timestamps, atomic writes, and every
mutation is validated with the runner's own engine (the submitted shape is
kept - the normaliser's output is not re-normalisable: it fills defaults
like an 'any' PoR check that the check normaliser then rejects), so a stored
script cannot fail at run start for a validation reason. Import accepts the
old localStorage export as-is (export/import remains the migration path; the
PWA import also takes an exported array).
- /api/test/scripts (GET card-free: path/count + scripts), POST create,
/update, /delete and /import serve the store; the endpoints answer 503
without a store and 500 on a write failure, like the preset endpoints.
- /api/test/run takes a stored script_id (resolved when no inline script is
sent); the PWA sends the edited copy inline plus the id for the report,
and the run snapshot carries script_id.
- The PWA's Test script pill loads from the store, writes through on edits
(debounced name, immediate step/new/copy/delete), reports load/save/delete
failures inline, and exports the portable {name, steps} shape.
Tests: 808 frontend + 701 Python; sw cache simple-v379.
Three gaps blocked SCP80 applet testing from the PWA:
- testRunPreset() still marshalled the pre-v3.8 flat preset fields
(kic/kid/keys/counter/tar/spi1/spi2, all empty on a stored preset), so
every SCP80 run failed with "SCP80 preset is incomplete: no keyset is
defined". The PWA sends the matched preset's id now and the server
resolves it from the store (_test_run_preset_from_body; the inline
`preset` body stays for API callers), so a run always uses the stored
keysets, TARs and counters. Regression tests on both sides.
- testscript._normalise_params dropped the scp80 step's `kvn` (the editor
exposed it and the runner and validation consumed it, so scripts
silently used the preset's first keyset). kvn is validated (1-15) and
passed through; the client-side problem check bounds it too.
- The PoR delivered as a SEND SHORT MESSAGE (PoR-in-submit, SPI2 bit 0x20)
is decoded for the expect step that fetches it: the new `por` content
check asserts the response status / R-APDU SW / response data, decoded
with the preceding scp80 step's context (concatenated SMS accumulate
across expectations). The inline transport stays on the action
(check.por / check.data).
- The new `files` content check asserts the File List of a REFRESH
(TS 102 223 8.18, tag 12/92) - e.g. qualifier 00 = NAA initialization
with full file change notification - as an order-insensitive set of FID
paths.
The step editor exposes both checks; help EN/RU, docs/api.md and the new
authoring reference docs/test-scripts.md document the format. Tests: 803
frontend + 680 Python; sw cache simple-v378.
The ladder only advances on cntr_low; any other verdict stops the search.
A stop on a non-por_ok verdict after at least one cntr_low keeps the value
anyway - the earlier rejection proves it is above the card's counter - but
the response reports success: false with counter_saved, the verdict and the
saved counter ("something is likely wrong with the security settings");
the PWA shows it amber. Without a prior cntr_low nothing is saved, and a
missing PoR stays an error. actual_response_sms_submit/ussd count as
accepted.
Docs: docs/api.md, repo AGENTS. sw cache simple-v377.
Tests: 672 Python + 798 frontend green.
_counter_probe_params validates SPI2, forces b1 (PoR required; a caller's
00 becomes 01) and defaults to the RAM listing transport 21; the no-PoR
error names the effective SPI2, and ramSyncCounter posts
ramListingSpi2('40') for the RAM view (the failed operation's transport).
Live 2026-10-02 (NP BAP): the probe had sent the plain SCP80 form's 00
(no PoR) - the card answered the ENVELOPE 9000 and sent nothing, so the
probe stopped after one packet with "card verdict: no PoR".
Docs: docs/api.md, repo AGENTS. sw cache simple-v376.
Tests: 670 Python + 798 frontend green.
The release selector lives in the preset rather than a global chip: presets
already travel with the card (ICCID auto-selection, export/import), and the
operations that will consume the release are preset-aware.
- presets.py: the optional `release` field (4-18; empty = unspecified) joins
PRESET_FIELDS; a value outside 4-18 is refused on save/import; the store
schema version is 5.
- catrelease.py: DEFAULT_RELEASE = 12 and preset_release() - the effective
release is the preset's own value, else Rel-12 (the safe level for cards
whose release is not known; an unset release never filters anything away).
The PWA mirrors both (CAT_RELEASE_DEFAULT + presetRelease()), drift-guarded.
- Cards tab: the editor's header row gains a compact CAT release select
(unspecified / pre-Rel-6 stored as 5 / Rel-6 ... Rel-18); the empty option
names the Rel-12 default and the tooltip explains the guard; the field is
collected, cleared and restored with the other form fields.
- Nothing filters yet: the Simulator's event parameters (stage 3) will use
the effective release.
- docs/api.md + help EN/RU updated; sw cache simple-v368.
- the ADM input capped the key at 8 chars, but the server's _verify_adm
takes 4-16 hex digits (padded to 8 CHV bytes) and the old input had no
limit; maxlength=16 now, with a static guard in html.test.js.
- cardsTarAdd/cardsTarRemove re-rendered the table from the save-path
reader, so an emptied role field was silently refilled with its default.
The renderer no longer invents defaults; a raw reader (cardsTarsFromTable)
feeds Add/Remove and the language-switch refresh, while the save path
(cardsTarsFromForm) keeps the default-filling for the mandatory roles.
- the editor's dynamic TAR/keyset rows now rebuild on a language switch
(refreshDynamicI18n) from what is typed - their placeholders and titles
carry t() strings. (The title/Save label were already refreshed there.)
- dropped the now-unused cardsRoleMsl helper and its test references.
- docs: the ADM is documented as 4-16 hex digits (help EN/RU, api.md,
AGENTS). sw cache simple-v362.
Tests: 665 Python + 784 frontend green; the whole inline PWA script
syntax-checked.
The Cards tab now shows the preset list by default; "Add preset" / "Edit"
open a separate editor view (Save stores and returns to the list, Cancel
discards; ramGoToPreset opens it too). The editor keeps the name/ICCID
header (with From card) and groups its fields into bordered blocks:
- PIN/ADM: the new optional card codes PIN1/PUK1/PIN2/PUK2 (plain text,
4-8 decimal digits) plus ADM, moved out of the top row; nothing consumes
the PIN/PUK codes yet.
- TAR: one uniform table (TAR, MSL, description); the first three rows are
the fixed role entries with the immutable English descriptions "ISD
compact format" / "UICC shared FS RFM compact format" / "ADF FS RFM
compact format"; further rows are deletable with an optional description.
- SCP80 (keyset editor) and SCP81 (PSK pair) as before.
Store: PRESET_FIELDS gains pin1/puk1/pin2/puk2 (optional; whitespace
stripped; 4-8 decimal digits validated on save and import), SCHEMA_VERSION
4, docs/help/AGENTS updated. TAR rows use unified ids (cards-tr-N-*), the
editor switches via cardsShowList/cardsShowEditor.
Tests: 665 Python + 783 frontend green; the whole inline PWA script
syntax-checked. sw cache simple-v361.
Stage 1 of the release guard: which proactive command, event, data object and
PLI qualifier appeared in which release.
Sources: the change histories of the three pinned specs - 3GPP TS 31.111
Annex W, ETSI TS 102 223 Annex V and ETSI TS 101 220 Annex Q. The version
major equals the release (V7.x = Rel-7 ... V18.x = Rel-18); ETSI aligned its
numbering with 3GPP, but the documents differ, so every row carries its source
document.
Content: curated tables for the 32 events, the proactive commands, the data
objects/TLVs the tool builds, the PLI qualifiers and selected TERMINAL PROFILE
features (release + source/CR per row; 'baseline' = the original Rel-4 spec,
'?' = not pinned down by the histories), the per-release feature entries of the
three histories, and a machine-readable JSON summary as the seed for the
code-side table (catrelease.py + the PWA mirror).
The data keeps the real release (Rel-4/5/6 ...); the UI will collapse
everything before Rel-6 to "pre-Rel-6".
- /api/ram-install-app referenced undefined spi1/tar (the resolution block
was only added to /api/ram-install and /api/cap-compat), so every request
answered 500. The same preset/MSL resolution is now in place, with
regression tests for both modes (install / make selectable) and for the
no-MSL refusal before any card I/O.
- esc() now escapes " and ' too: the TAR editor renders free-text
descriptions (and other values) into attribute contexts, so a crafted
preset import could inject attributes. Three tests asserting the old
unescaped output updated; a dedicated esc test added.
- _spi1_for_tar names the no-TAR case in its hint (a pre-built packet
without tar/spi1 used to report "TAR ?").
- preset free TAR rows no longer store an empty "role" key (the documented
shape); a non-list `tars` in an update is refused (400) instead of being
silently ignored.
Live: the no-MSL request answers 400 with the hint; a make_selectable with
a bogus AID runs the format probe and the INSTALL (derived SPI1 16 from the
preset MSL), the card answers remote SW 6A88 and the counter advances - no
500. Suites: 658 Python + 775 frontend.
No version bump: completes the unreleased v3.10.0 (6360dc5 was never
pushed). sw cache simple-v359.
Presets drop the card-wide SPI1/SPI2 and the fixed tar/uiccTar/usimTar
fields: a preset now carries `tars` - the three mandatory role entries
(isd/uiccRfm/usimRfm, spec defaults pre-settled) plus free TARs with an
optional description, each with its MSL (Minimum SPI1, TS 102 226
8.2.1.3.2.4). v3.9.1 presets convert on load/import (the spi1 seeds all
three MSLs); duplicate TARs and empty TAR/MSL values are refused; the
store schema version is 3.
Operations derive a packet's SPI1 from its TAR's MSL (_spi1_for_tar): the
RAM ops, the test runner (per-step overrides still win), the counter probe
and each TAR-probe row; the plain send keeps an explicit SPI1 with a
`warning` when it is below the MSL, and a TAR outside the preset without an
explicit SPI1 is refused with a hint. The TAR probe resolves per checked
TAR before sending and reports the effective spi1 per row. The secured
packet form's SPI2 default is 00 (the RAM/probe operations pin their own).
PWA: the Cards form edits the TAR table (role rows with MSL inputs plus
Add/Remove free rows with a description), the list shows a TARs (MSL)
column, the SCP80 TAR field suggests the preset's entries (datalist) and a
preset apply seeds the SPI1 from the operation TAR's MSL with an amber
warning below it; the preset-wide SPI1/SPI2 fields and the header marker's
SPI requirement are gone (completeness = a keyset + the three role entries
with their MSLs). Help EN/RU, docs/api.md and AGENTS updated; sw cache
simple-v358.
Live: TAR probe 000000 at its MSL 16 (registered, por_ok, counter
advanced); a send without spi1 derived 16 (por_ok, counter advanced); a
send with spi1 0A returned the warning and the card refused it (6200, no
counter consumed); an unknown TAR without spi1 was refused with the hint.
- The probe no longer takes a caller-supplied `apdu`: it always sends the
read-only SELECT MF (the counter advances on the packet's security
acceptance whatever the applet answers - 6A86/6D00 are normal there), so
no caller can turn the sync probe into a write.
- Version 3.9.1: server VERSION, pyproject.toml and the PWA header; sw cache
simple-v357; docs/api.md and the repo AGENTS.md updated, and the workspace
AGENTS.md gains the v3.9.1 history entry (its "Currently" line was stale at
v3.7.1).
Live findings that drove this: the "card counter 0000000000" the low-counter
notice showed is not the card's counter at all (the PoR's CNTR is a copy of
the command's counter, TS 102 225 5.2 table 3, and a rejection leaves it
zeroed); and an earlier "no card accepts the expanded format" observation was
a hand-made template whose declared length did not match its body - the card
validates the lengths and answers with a Bad format TLV.
- _cntr_low_fields(status) reports the verdict as a flag; card_cntr/
suggested_cntr and the UI's "(card counter ...)" text are gone - the value
is not observable. The notice offers a bounded Sync counter probe instead.
- POST /api/counter-probe: a doubling counter ladder (start+1, +2, +4, ...)
with a read-only command until the card accepts one, then accepted+1 is
persisted (monotonic, source counter-probe). Rejected attempts change
nothing on the card; the search stops at a ceiling (default FFFFFFFF, far
below the 40-bit maximum where the counter blocks) and after max_attempts
(default 40, clamped 1-64); a non-cntr_low failure stops it at once.
- _parse_response_scripting decodes the Bad format TLV (90, TS 101 220 table
7.20; error types per TS 102 226 table 5.12) and returns a dict; the PoR
carries bad_format/bad_format_name and _ram_step_result fails the step with
"bad format 02 (wrong length)" instead of falling through to the compact
decoder (which read the AB tag as 171 commands).
- _wrap_expanded_apdu(apdu, form) emits the definite (AA) or the indefinite
(AE 80 ... 00 00) scripting template; the RAM command formats are compact/
expanded/expanded-ae and _ram_detect_format probes compact, then both
expanded codings (the expanded probes use Le='00' and no chained GET
RESPONSE, TS 102 226 5.2.1.1). The RAM form gains a Format selector.
- Live-verified: the verdict carries no value; the probe walked
0x1000001/0x1000003 (low) then 0x1000007 (accepted, stored 0x1000008); the
ISD executes both expanded codings (R-APDU 08A0000000030000000F80, SW 9000)
and a length-mismatched template answers bad_format 02 - decoded now.
- Tests: the ladder, the probe walk + persistence, params validation, both
template forms, the Bad-format TLV (definite and plain tag styles), the
live ISD response through _decode_por, the step-result failure, and the PWA
guards; help EN/RU, docs/api.md, the repo AGENTS.md; sw cache simple-v352.
Documentation:
- help (§1/§3): the SCP80 tab is switched by three pills - the tab list and the
section intro now name the TAR probe (RU "TAR-сканер"); §3.1/§3.2 describe the
Keyset selector next to the preset selector (and that a successful PoR
advances the counter only when SPI1 carries a counter check); §4 describes
the keyset editor rows, the derived number and the per-keyset counters; the
probe section keeps its counter rule.
- README/README_RUS: the SCP80 tab says three pills and gains a "TAR probe" /
"TAR-сканер" section (standard list, verdicts, counter persistence); the
Cards section is rewritten for the server-side store - keysets, server-owned
per-keyset counters with the audit trail, JSON export/import as the migration
path, the keyset editor, the equipped-card mismatch banner - and the field
table, "Add a card" and "Edit / remove" follow.
Terminology:
- "key version" is no longer used for SCP80 keysets in the UI, the server error
strings and the docs: the selector is "Keyset", its options "Keyset 1 - ...",
the messages say "keyset N ...". The TS 102 225 A.1/A.2 quotes and the GP
PUT KEY P1 field keep the spec's own wording.
- RU keeps the word "keyset" untranslated ('Добавить keyset', 'Номер keyset',
"не является keyset пресета", ...) and uses "пресет" instead of
"предустановка" throughout the dictionary, the help and README_RUS.
sw cache simple-v340. Tests: 742 Node / 619 Python.
Three things learned from probing a live card, plus the capability they enabled:
1. Counter tracking follows SPI1.b5b4 (TS 102 225 5.1.1): with 00 the counter
is "present, ignored, never updated", so a packet without a counter check
must leave every counter untouched. _counter_tracked() now guards
/api/send-ota (no final_cntr, no persistence, a log line), the RAM step
sender and the test runner; _preset_counter_persist() requires a non-zero
key version (the "single keyset" fallback could advance a keyset spuriously
for a keyless send). The PWA advances the form counter only when the server
reports final_cntr and says "counter not tracked" otherwise.
2. The spec's no-security form builds again: _ota_keyset() refused the valid
KIc/KID '00' (TS 102 225 A.2: '00' is legal with SPI1.b3=0 for KIc and
SPI1.b2b1=00 for KID) because pySim wants an algorithm. An unknown nibble
is now refused only when the algorithm is actually used (ciphering / a
ciphered PoR for KIc, RC/CC/DS / a MAC'd PoR for KID) and 'implicit' codes
the nibble as 0 otherwise - POST /api/send-ota {apdu, spi1:"00", kic:"00",
kid:"00"} now builds 0D00010000B0000000000000010000A40000, the exact packet
the card accepted (a fake 15/15 was refused with 6200). A no-security PoR
is decodable again too, and a requested-but-missing PoR sets por_missing
on the plain path as well.
3. TAR probe (POST /api/tar-probe, RAM op "Probe TARs"): one secured packet per
TAR - the standard allocations of TS 101 220 V18.3.0 Annex D, Table D.1
(TAR_PROBE_TARS), or a custom list - carrying a harmless C-APDU (SELECT MF)
with the preset's keyset and SPI 16/01. Each TAR is classified
(registered / no_answer / no_por / refused) from the ENVELOPE verdict and
the application's PoR, counters advance and persist normally, and the run is
refused when SPI1 has no counter check. Live on a card: 000000, B00000 and
B00001 registered (compact-format applications, answering 6D00 / 6B00),
the expanded-format TARs, USAT, multiplexing and CASD refused with 6200 -
with the keyless form the same card accepted every TAR and answered no PoR.
Docs (api.md, help EN/RU, AGENTS), sw cache simple-v337. Tests: 618 Python
(no-security build, the algorithm-only-when-used rule, the tracking rule, the
persist guard, the probe classification) and 736 Node (the probe renderer and
wiring, spCounterTracked).
A card usually holds several OTA key sets, and the SCP80 counter is tracked
per key version on the card, so a preset now carries one keyset per
GlobalPlatform key version - the b8..b5 nibble of KIc/KID (TS 102 225
5.1.2/A.2), the version is implicit in the bytes. The spec is explicit:
"a dedicated counter shall be associated to each key version" (Annex A.1) and
"the key version number indicated in the KIc and KID (bits b8..b5) fields
shall be identical when different from 0" - a mismatch is rejected by the card
with "Unidentified security error" (A.2).
Server (presets.py, schema 2):
- a preset carries `keysets: [{kic, kid, kicKey, kidKey, cntr}]`; validation
refuses a KIc/KID version mismatch (A.2), a version outside 01-0F ('00'
means "no security" and is a packet-level choice, not a keyset), duplicate
versions and missing keys/counters; a v3.8.0 flat preset converts into one
keyset on load and on import;
- `set_counter(..., kvn)` writes the counter of that keyset (monotonic per
keyset), the audit line carries the kvn, and `find_keyset()` serves the
lookups;
- the operations derive the key version from the request's KIc byte and
persist into that keyset: /api/send-ota, the RAM install/install-app/compat
steps (_ram_send_gp_apdu) and the test runner's SCP80 steps (which may name
a `kvn` per step and keep one working counter per keyset);
- a non-zero key version the named preset does not define, or a KIc/KID
mismatch, is refused before anything is built (_preset_keyset_check /
_keyset_guard: 400, or a step error in the test runner). Without a
preset_id only the mismatch rule applies.
PWA:
- the Cards form's KIc/KID/counter/keys inputs become a keyset editor (one row
per version: KIc, KID, both keys, that version's counter, Add/Remove; the
version is shown derived from the bytes and a mismatch is flagged);
- the SCP80 and RAM views get a Key-version selector beside the preset
selector, driving the KIc/KID bytes and loading that keyset's keys and
counter; the packet builder refuses a version the preset does not define
(spKeysetCheck/spKeysetGuard, pure and unit-tested);
- the counter syncs are per keyset (spCntrLocalSync/ramSaveCntr), the SCP80
form's counter edit posts the keysets, and the test-run write-back syncs
every counter the server reports (scp80_counters).
Docs/help/AGENTS updated; sw cache simple-v335. Tests: 610 Python (store
keysets, endpoint refusals, the counter landing in the right keyset) and 726
Node (keysets.test.js plus the updated harnesses).
- /api/send-ota no longer 500s on a counter-less body: the plain `sp` path
reports final_cntr only when the counter is usable (_counter_valid, 1-10 hex
digits) - before, the new plain-path report ran int('') and the request died
in the generic handler. The failure path is guarded the same way.
- _decode_por wraps the keyset/SPI construction in the same "never raise" rule
as the decode: a malformed counter or missing keys return None instead of
escaping the handler.
- presets.add() never accepts a taken id (a fresh uuid is assigned, like the
import path), the counter is validated as 1-10 hex digits instead of being
silently masked/kept, and set_counter() ignores a malformed value with a log
line instead of writing it.
- the preset endpoints answer a JSON 500 when the store write fails (read-only
home, disk full) instead of dropping the connection.
- tests: counter-less send-ota, store-write 500, duplicate id, counter
validation (add/update/set_counter), and the temp-file check no longer
matches the audit file; docs/api.md documents final_cntr for the plain path
and preset_id.
The presets lived in the browser's localStorage and the PWA wrote the SCP80
counter back after every operation, so an increment could be lost when the
response never reached the tab, when two browser contexts (or the demo origin
plus a local one) held separate copies, or when the tab was closed mid-run.
The counter is card state and the server performs every card operation, so the
store now lives next to the server:
- pysim_simple_server/presets.py: one JSON file
(~/.pysim-simple-server/card_presets.json, --card-presets overrides; Path.home()
resolves the same way on Windows), atomic writes (temp + fsync + os.replace),
an RLock, stable uuid ids, the ICCID normalisation the PWA uses (digits /
spaced / raw EF hex), a duplicate-ICCID refusal and a monotonic set_counter;
every counter change is appended to card_presets.json.audit.jsonl.
- API: GET /api/presets (card-free), POST /api/presets and
/api/presets/update|delete|import; the import also takes the old localStorage
export shape - that is the migration path, there is no automatic migration.
- Operations name a preset_id and the server persists the counter they
consumed: /api/send-ota (the plain SCP80 path reports final_cntr now too),
the RAM install/install-app/compat steps (_ram_send_gp_apdu) and the test
runner's SCP80 steps (_preset_counter_persist).
- PWA: the Cards tab is a thin client of the store (load on connect, CRUD via
the API, export/import), the counter write-backs became local display syncs
(the SCP80 form's counter input stays a deliberate human edit) and the
SCP80/RAM bodies carry preset_id; a new pysimFetchOk() surfaces the server's
error bodies instead of silently using them.
Policy: card keys in logs and local files are not a risk for this workbench
tool (AGENTS.md) - the store is not encrypted and no redaction is required.
Tests: tests/test_presets.py (store: ICCID forms, counter monotonicity and
wrap, atomic writes, corrupt-file recovery, import), the preset endpoints in
tests/test_fetch_routes.py (incl. the counter persistence via /api/send-ota)
and the updated PWA tests. sw cache simple-v334.
The compatibility test now runs in the same operation modal as the installs
(live progress from the server's ram_progress, Close disabled until done) and
its result gains a per-AID detail block instead of the comma list:
- the probe summary (probe: 2 imports -> 0.0 . +1 appended);
- the import list as tested (ramCompatImportRows): the CAP's imports with the
version the card was asked for and the CAP's own version when the probe
lowered it, appended synthetic entries and unmatched lines - each with the
resolved library name (jcAidName), so a raw AID list becomes readable rows;
- on a rejection, the failure analysis (ramCompatFailureHtml): which CAP
section the failing block covers (load-file byte range and component names,
'completes Import'), what the card checks at that point (JC VM spec 4.5.2)
or that the import list already passed, the remote status word with its
name, and what the probe state implies (every import at 0.0 -> a missing
package or another load-file check is the cause; otherwise lower the
versions and bisect).
ramCompatVerdict names the remote SW; the inline result keeps the verdict +
short probe note and the modal carries the escaped step lines + the detail
HTML (ramOpModalFinish gained an asHtml mode); the server reports
probe_unmatched as [{'aid', 'version'}] (_probe_unmatched), documented in
docs/api.md. The same failure block for the real install is the next step.
sw cache simple-v325; tests: 550 Python / 703 Node.
The compact (legacy) GET STATUS entry is <aid_len><AID><life cycle>
<privileges> + (P1=10) <module count> (<len><module AID>)*, cross-checked
against GlobalPlatformPro's populate_legacy (the 4th byte is privileges;
0x00 is required for load files). The parser read it as a 'version' one
byte late, so ELF rows showed the module count / the next entry's length as
Version: 00/10 - the compact listing has no version at all; it now comes
from the expanded CE tag (ramFmtVersion: 2 bytes -> major.minor, GP Table
11-37 Note 1) and merges (the bogus compact value no longer blocked it).
New data exposed:
- EA SCP Registry Data (TS 102 226 8.2.1.6) -> 80 Menu Parameters -> the
applet's STK menu entries: STK menu entry: 0x80 (position 1, Enabled)
- unknown registry objects are preserved (rawTlvs) and shown as
'Other registry data: 8F=752B . 90=012D ...' instead of being dropped
- 84 parses the GP coding (one Executable Module AID per TLV, repeated; the
concatenated 4F<len><aid> form stays as fallback) and the ELF expanded
queries add 84 to the tag list (with a tag list the card returns only the
listed tags - the fallback path would otherwise lose the module lists and
their Install buttons)
- life cycle names decode per object kind (GP Tables 11-3..11-6: ISD/card,
app, SD - detected via the Security Domain privilege - and ELF), with
multi-byte 9F70 (02 0700 on live cards) decoding the first byte and
surfacing non-zero extras; the R-APDU parser's LIFECYCLE_MAP labels were
aligned with the same tables
/api/ram-install measures the NV footprint: a silent GET DATA FF21 read
before and after the chain (also on failure) returns nv_before/nv_after/
nv_delta; the reads never fail the operation (no FF21 -> no fields) and the
PWA shows 'NV used: ~N B (free A -> B)' in the install result.
sw cache simple-v324; tests: 548 Python / 698 Node.
Comparing the new TCA vendor traces (Explore_EP/ET/NC/NP/SE) with our flow
showed why only one card works:
- The PoR capture was gated on SPI2 bit 0x20. The traced cards deliver the
PoR as a proactive SEND SHORT MESSAGE; with our ISD/memory queries
(SPI2=0x01) the fetched TPDU was thrown away - the Explore died with
"no data" and the RAM install steps silently degraded to an unverified
"no_por". The capture is now unconditional (inline ENVELOPE responses and
proactive submits both work), and the STATUS-poll fallback is enabled for
every card.
- `_ram_next_cntr` still wrapped at 32 bits (`% (2 ** 32)`): a 10-digit
counter like 10000AAAC8 came back as 00000AAAC9 after a RAM/Explore step -
the server-side half of the v3.6.23 counter report. It now advances the
full 40 bits.
- RAM command format (TS 102 226 5.2.1: compact C-APDU vs the expanded
`AA` > `22` form) is detected per operation and never stored:
/api/ram-install and /api/ram-install-app always probe, /api/send-ota
probes when the caller passes `ram_format`. The probe is a read-only
GET STATUS [ISD] as the first step (compact, then wrapped) and every
command of the operation uses the detected format. `_wrap_expanded_apdu`
is pinned to the reference bytes `AA0A220880F24000024F0000`; responses
carry `ram_format` and `final_cntr` (accepted probes consume counters, so
the views use the server's next counter). Applet-directed payloads stay
byte-exact; only RAM views (and RAM/GP chain packs) opt in.
504 Python / 653 frontend green; version 3.6.24; sw simple-v297.
Iterating the final install step no longer needs a delete + re-load of the
.cap:
- `POST /api/ram-install-app` runs INSTALL [for install] (`mode: "install"`,
P1 0x0C/0x04 by make_selectable) or INSTALL [for make selectable]
(`mode: "make_selectable"`, GP 11.5.2.3.3 Table 11-44) for an already
loaded package. The APDU builders are shared with `_cap_apdu_sequence`
(`_cap_install_apdu`, `_cap_make_selectable_apdu` - one builder, no drift)
and the per-step send/PoR/counter logic is the extracted shared
`_ram_send_gp_apdu` (used by /api/ram-install too).
- PWA: the RAM op selector gains "INSTALL [for install] (loaded package)"
and "INSTALL [for make selectable]" with Load File / Module / Instance AID
inputs (instance defaults to the module AID); the result renders through
the shared `ramShowInstallResult` and `final_cntr` keeps the preset in
sync.
- Explore: an **Install** button on a package's module AIDs that have no
instance yet, and a **Make selectable** button on INSTALLED (0x03)
instances - both switch the op and pre-fill the AIDs.
- tests: the install APDU pinned to the live no-STK packet and to the
sequence tail, the make-selectable APDU, ramHasInstance, the Explore
buttons and the op wiring; docs/api.md updated.
650 frontend / 498 Python green; version 3.6.18; sw simple-v291.
Explore still missed the F0414C46416101 package on a card whose responses
wrap the R-APDU in the TS 102 226 5.2.2 Response Scripting template
(`AB <len> 80 <count> 23 <len> <R-APDU>`): `_decode_por` parsed that as a
compact response, so the frontend got `last_status_word` 81d0/7680 and data
starting `80 01 01 23 ...` instead of the listing.
- server: `_parse_response_scripting()` (AB definite / AF 80 ... 00 00
indefinite) extracts the executed-command count and the last R-APDU's SW
and data; `_decode_por` exposes it as `response_type: scripting` in the
same `decoded` shape as compact, so the RAM explore paging and the RAM
install `por_sw` see the real 9000/6310.
- frontend: the compact listing walk is deterministic on the AID length
(`len AID life ver`; P1=10 adds `module_count (len module_AID)*`).
`_parseRawAppEntry` no longer guesses `rawLen-1` for >8-byte AIDs (16-byte
A113 applet AIDs were truncated), and `_parseRawElfEntry` no longer scans
for `0x10` (a length/AID byte equal to 0x10 derailed the walk: a live page
parsed to 1 entry with no F0414C46416101).
- tests: exact trace fixtures - the ELF page lists F0414C46416101 (11
entries), the P1=10 page attaches its F0414C4641610101 module, the app page
keeps the 16-byte A113 AIDs; Python covers the scripting template
(definite/indefinite) against the live `AB 12` ISD and listing vectors.
610 frontend / 495 Python green; version 3.6.5; sw simple-v278.
Explore returned partial data (no ELF/module entries, the installed package
missing) and then failed with cntr_low. Two causes, both in the
actual-response SMS-SUBMIT path the card uses for big listings:
- `_find_sms_tpdu` read TLV lengths as a single byte; the FETCH carries
`8B 81 97 ...` (BER long form) for the big pages, so it returned a
corrupted, truncated TPDU.
- `_calc_ud_offset` treated the SMS-SUBMIT relative validity period (VPF=10)
as 7 bytes instead of 1, shifting the UD offset: `_parse_sms_concat` then
read a bogus UDH and reported no concatenation, so the segments never
assembled and the PoR/data was dropped ("RAM RESPONSE-PACKET: empty").
The step was marked failed, the counter did not advance, the same counter
was retried and the card answered `cntr_low`, which also blocked P1=10
(modules) where the installed package appears.
- the captured segments now always store the assembled UD (`submit_ud_hex`);
`_sms_submit_por()` rebuilds the DELIVER-style packet (`02 71 00` + UD) for
`_decode_por`. Verified against the live capture: por_ok, remote SW 6310,
438 hex chars of listing data (the exact bytes of the P1=20 page).
- `spPorAccepted` counts `actual_response_sms_submit` (0x0B) as accepted, so
the counter advances when the data follows via SMS-SUBMIT.
Explore queries follow GP Card Spec v2.3.1 11.4.2.2: compact listings
(P2.b2=0) with the chained GET RESPONSE (`ramGetStatusApdu`, paging P2=00 ->
P2=01), the malformed P2=02 attempt is gone, and the ISD-only query (P1=80)
never pages with next-occurrence (the card shall reject it).
Tests: Python SmsSubmitCaptureTest with the live FETCH bytes (TPDU length,
UD offset, concat reassembly, 027100+UD decode); ram.test.js checks the APDU
builder and the missing P2=02 attempt; cards_counter covers 0x0B.
608 frontend / 492 Python green; version 3.6.4; sw simple-v277.