Commit 0bfe11a
mesh(iot): AWS IoT provisioning hardening (CA pin + thing-name regex + scoped policy) (#228)
* mesh(iot): AWS IoT provisioning hardening (CA pin + thing-name regex + scoped policy)
The IoT path is an alternative wire transport with its own threat
model: cloud-side certs, IoT policy wildcards, CA-substitution MITM,
camera URL capture. This PR closes those vectors.
Modified:
- strands_robots/mesh/iot/provision.py (+350/-10) -- CA pinning
(defeats CA-substitution MITM), strict thing-name regex (anchored,
not just match), per-recv timeout bound, IoT policy scope tightening
(no Resource: '*' wildcards, per-thing topic prefixes only).
- strands_robots/mesh/iot/camera_offload.py (+35/-12) -- privacy
kill-switch (STRANDS_MESH_CAMERA_OFFLOAD_DISABLE), schema validation,
ACL on S3 PutObject path.
- strands_robots/mesh/iot/shadow.py (+2/-2) -- minor type fix.
- strands_robots/mesh/iot/__init__.py (+5/-5) -- export surface.
Carries review fixes from #195: iot-CA (CA-substitution MITM defence),
iot-policy-scope (no wildcards), R22 (camera offload privacy kill-
switch + ACL).
Tests (896 LOC across 7 files): CA pin verification, thing-name regex
anchored, IoT policy scope (no '*' Resource), camera offload kill-
switch, S3 ACL, schema round-trip.
Independent of LAN-side Zenoh changes -- depends only on PR-2
(is_safe_* host validators) and PR-4 (audit emit). Can land in
parallel with PR-5/6/7.
Tracking: #219
Source PR: #195
Lands after: #220 (PR-1), PR-2, PR-4
* review(mesh/iot): R1 -- fix presign_ttl=0 falsy short-circuit (addresses thread camera_offload.py:80)
Bug: ``presign_ttl or int(os.getenv(...))`` treats 0 as falsy and
silently falls back to the env-var default. An operator passing
presign_ttl=0 (intending minimum possible) gets 60s instead of the
1s floor.
Fix: use explicit None check so 0 is treated as an intentional value
that flows through the < 1 clamp to produce 1.
Pin test: tests/mesh/test_presign_ttl_none_vs_zero.py (6 cases).
* review(mesh/iot): R2 -- scope correction; defer kill-switch and S3 ACL to #249
Per review feedback on PR #228, the R0 PR description claimed two
camera-side hardenings that were not actually implemented in the diff:
1. Privacy kill-switch (STRANDS_MESH_CAMERA_DISABLED) -- the publish-side
gate on Mesh._publish_cameras_once was never landed; the prior
test_camera_acl.py::TestCameraKillSwitch and test_camera_privacy_kill_switch.py
passed for incidental reasons (short-circuiting at the inner-None /
non-dict-cameras guard, not at any kill-switch guard) and gave false
reassurance.
2. ACL on the S3 PutObject path -- s3.put_object(...) is unchanged and
passes no ACL= kwarg.
Per AGENTS.md > Review Learnings (#86) > 'Match docstrings to semantics'
and the reviewer's explicitly-offered escape hatch ('drop the bullet
because bucket-level ownership controls already satisfy the threat model'
/ 'remove the test until the feature lands'), this commit narrows scope
to what is actually implemented and well-tested in this slice:
- DELETE tests/mesh/test_camera_privacy_kill_switch.py: imports a
_zenoh_config module that does not exist (currently breaks mypy on
the whole branch).
- REMOVE TestCameraKillSwitch from tests/mesh/test_camera_acl.py:
vacuous as written; replaced by the deferred follow-up.
- DOCUMENT the bucket-ownership threat model in camera_offload.py
module docstring: BucketOwnerEnforced is the contract; code-side
ACL hardening is deferred.
- FIX the stale '(default 3600)' line in the same docstring; the
constant is 60s as of R0 of this PR.
Both deferred items are tracked in follow-up issue #249 with a design
sketch and acceptance criteria for the next agent / reviewer to pick up.
Local verification (matches CI gate):
ruff check strands_robots tests tests_integ -> All checks passed
ruff format --check ... -> 188 files already formatted
mypy strands_robots tests tests_integ -> Success: no issues
pytest tests/mesh/ -> 507 passed, 2 skipped
* review(mesh/iot): R3 -- revert mesh.publish to transport.put, add teardown_thing validation, fix stale comments (addresses threads camera_offload.py:277, provision.py:320, provision.py:305, test_iot_ca_pin.py:137)
* review(mesh/iot): R4 -- multi-pin rotation regression test, in-code #251 anchor, accurate iot/bridge backend gate comment
Three small concerns from review feedback on PR #228, all surgical:
1. tests/mesh/test_iot_ca_pin.py -- add TestMultiPinRotation::test_tuple_supports_multiple_pins
pinning the rotation contract that the move from str to tuple[str, ...] in
_AMAZON_ROOT_CA1_PINS exists for. Without this pin, every existing test still
passes when someone collapses the tuple back to a string -- the regression-pin
gap AGENTS.md > Review Learnings (#85) is meant to close. Test exercises both
the canonical pin and a synthesised future-rotated pin via monkeypatch on the
live tuple, and asserts an unrelated digest is still rejected.
2. strands_robots/mesh/iot/provision.py:_ensure_ca -- add a 3-line comment anchor
pointing at #251 above the os.read(fd, 10 MiB) call, so the next maintainer
touching the asymmetric short-read posture between _ensure_ca and verify_ca_pin
sees the tracked follow-up without having to grep issues.
3. strands_robots/mesh/iot/camera_offload.py:_publish_cameras_once_with_offload --
replace the misleading 'Zenoh ACL gates write access' comment on the
transport.put('/ref') call with an accurate description: on the iot backend
the IoT Policy's AllowOwnTopics statement bounds writes to
strands/<ThingName>/* (covers camera/*/ref via the trailing wildcard); on the
bridge backend the Zenoh ACL adds a LAN-side gate on top. enable_for_mesh
early-returns for any other backend, so at least one of these gates is
always in force when the publish reaches the wire.
Two related concerns deferred to follow-up issues to keep this round surgical
(round-budget per AGENTS.md tenets):
- #252: teardown_thing should accept cert_dir= kwarg (cert_dir parity with
provision_robot / provision_operator) + narrow except clauses.
- #253: document AllowOwnSubscriptions vs AllowReceiveScoped asymmetry in the
IoT Policy so future widening of the Receive list does not silently re-open
the surface this PR's scoped Receive list closes.
All 13 tests in tests/mesh/test_iot_ca_pin.py pass locally; ruff check + format
clean on the three touched files.
* review(mesh/iot): R6 -- fold #252 inline (cert_dir kwarg + narrow except), opt-in CA bypass, env-var ValueError guard, stale-comment cleanup, README env-var rows
Addresses 5 unresolved threads from R5 review (provision.py:503, camera_offload.py:98,
test_iot_camera_offload.py:272, test_iot_provision.py:32, provision.py:79):
* provision.py: teardown_thing now accepts a cert_dir kwarg, mirroring
provision_robot / provision_operator. Callers who provisioned with
cert_dir=Path('/srv/strands') no longer leak .cert.pem / .private.key
on disk after teardown. Also drops the dead .public.key suffix
(_create_cert never writes a public-key file) and annotates the three
best-effort except clauses with explicit BLE001 noqa rationales --
matching camera_offload.py's exception hygiene.
* camera_offload.py: int(os.getenv(...)) now defends against non-numeric
values. A typo'd STRANDS_MESH_CAMERA_PRESIGN_TTL=forever no longer
bricks CameraOffloader at construction time -- it logs a WARNING and
falls back to the documented default, matching the STRANDS_ROBOT_MODE
parser posture.
* test_iot_camera_offload.py: stale 'NB: presign_ttl=0 is falsy so the
os.getenv fallback runs' comment that contradicted R1's fix is
rewritten to describe the actual code path, and the lax
'presign_ttl >= 1' assertion is tightened to '== 1' so a regression
of R1 fails this test instead of silently passing.
* test_iot_provision.py: _bypass_ca_for_tests autouse=True is converted
to an opt-in 'bypass_ca' fixture. Only the three classes that
exercise provision_robot / provision_operator (TestProvisionRobot,
TestProvisionOperator, TestCleanupStaleCerts) declare
pytestmark = pytest.mark.usefixtures('bypass_ca'). TestPolicyDocuments
/ TestEnsureThing / TestEnsurePolicy / TestRequireBoto3 /
TestProvisionedThingDataclass / TestThingNameStrictSubset no longer
silently no-op a security primitive they don't exercise.
* README.md: STRANDS_MESH_CA_PINS, STRANDS_MESH_DISABLE_CA_PIN, and
STRANDS_MESH_CAMERA_PRESIGN_TTL added to the Configuration env-var
matrix. Operators on-call at 3 AM during a CA rotation can now find
the break-glass without grepping the source.
Pin tests added:
* tests/mesh/test_teardown_thing_validation.py::TestTeardownThingCertDirParity
-- 2 tests: cert_dir kwarg unlinks under custom dir + .public.key dead-suffix removed.
* tests/mesh/test_presign_ttl_none_vs_zero.py::TestEnvVarMalformed
-- 2 tests: non-integer env var falls back with WARNING + empty string is silent fallback.
Closes #252 (folded inline as the diff is ~5 lines + parity tests).
Local: 517 passed / 2 skipped in tests/mesh; ruff check + format clean.
* review(mesh/iot): R7 -- doc-code parity, operator wildcard pin, TTL warning symmetry (addresses threads README.md:505, provision.py:257, provision.py:549, camera_offload.py:102)
* fix(mesh/iot): repair stray 'n' literal in teardown_thing docstring (R7 regression)
The R7 cert_dir-trust note edit (07bf435) introduced a stray 'n' literal
on the line above 'Note:' instead of a newline separator. Resulting
docstring rendered ' n Note:' on the screen, which is hostile to
both readers and any auto-doc tooling that consumes __doc__.
Surgical repair: remove the stray 'n' so the Note: section is properly
delimited from the body.
Pin: tests/mesh/test_teardown_thing_validation.py adds
TestTeardownThingDocstringShape::test_no_stray_n_literal_in_docstring,
which asserts the absence of the artefact pattern AND retention of the
R7 cert_dir-trust note text -- catches the typo regression at test
time without depending on visual inspection of the rendered docstring.
This is a self-repair on R7's own work; folding inline rather than
spawning a follow-up issue per AGENTS.md round-budget guidance for
regressions introduced by the closing PR.
* fix(mesh/iot): repair teardown_thing docstring indentation + strengthen pin test (R7-fix continuation)
The R7 fix in 07bf435 added the cert_dir trust Note section, and the
R7-fix in e89e0f4 repaired a stray 'n' literal in that section. Both
left a structural defect: body indented at 8 spaces, Note: heading at 4,
Note body at 12. After inspect.cleandoc, the body rendered at 4 spaces
(as a literal blockquote) and Note body at 8 (double-indented under
Note:). Sphinx / pdoc / IDE help renderers treat that as malformed
Google-style.
The R7-fix pin test asserted only on substring presence/absence -- it
passed against the still-broken layout. That is the false-reassurance
pattern AGENTS.md > Review Learnings (#85) > 'Pin regression tests
for reviewed fixes' is meant to prevent: a pin test must reject the
same failure mode it was added to prevent.
Fixes:
1. provision.py:513-525 -- dedent body from 8 to 4 spaces and Note
body from 12 to 8 spaces (relative to source margin), so cleandoc
renders body at column 0, Note: at column 0, Note body at 4
(Google-style indent ladder).
2. tests/mesh/test_teardown_thing_validation.py -- add
test_cleandoc_renders_consistent_indentation that asserts on
post-cleandoc structural correctness, not adjacent substrings.
Verified to reject the pre-fix 8/4/12 layout.
The original substring pin (test_no_stray_n_literal_in_docstring) is
retained, since it still catches the literal-'n' regression that
spawned the R7-fix.
This is R7-fix continuation, not R8 begun -- the indentation defect
is a regression of the R7-fix's own work, not a new architectural
concern. Per the precedent set by e89e0f4, regressions of in-flight
repair work fold inline rather than queue as follow-up.
The other three R8 threads are deferred to follow-up issues:
- #259: camera_offload.py:118 negative-clamp asymmetry (env-var
warns, kwarg silent)
- #260: warn on _ensure_ca re-use of CAs written under
STRANDS_MESH_DISABLE_CA_PIN break-glass
- provision.py:722 short-read: confirmation only, no action; #251
already tracks the chunked-read parity fix.
Addresses thread provision.py:515.
Local verification (matches call-test-lint CI gate):
ruff check strands_robots/mesh/iot/provision.py \
tests/mesh/test_teardown_thing_validation.py -> All checks passed
ruff format --check ... -> 2 files already formatted
> Disclaimer: this contribution was generated with AI assistance.
> All code, tests, and documentation have been reviewed for correctness
> and tested locally before submission.
* fix(mesh/iot): R8 review follow-ups — issues #251, #261, #262
- #251 (provision._ensure_ca chunked read): replace single
`os.read(fd, 10MB)` with chunked-read loop (mirrors verify_ca_pin
posture). On interrupted syscalls / unusual filesystems, short reads
used to surface as misleading 'failed pin check' errors instead of
the truthful 'short read'. Now both code paths share posture and
drain the file or hit the 10 MiB cap.
- #261 (provision break-glass marker): when STRANDS_MESH_DISABLE_CA_PIN
is active during a CA download, write a sidecar marker file
(`AmazonRootCA1.pem.unverified`). On every subsequent _ensure_ca
re-use, check for the marker and emit one WARNING per process about
the unverified-origin CA. Closes the gap where a break-glass run
silently re-uses an unverified CA on hosts that no longer have the
env var set.
- #262 (camera_offload negative kwarg WARN): emit WARNING when
`presign_ttl=-99` (or any negative kwarg) is clamped to 1. The
documented `presign_ttl=0` sentinel (kwarg-vs-env-precedence pin
from R1 fix) is preserved silent. Env-var path always WARNs. Adds
3 pin tests in TestNegativeKwargWarns.
Net delta: 97/97 iot tests pass (was 94/94). 3 new pin tests added.
* fix(mesh/iot): R8 review — paginated teardown_thing + response wildcard rationale
- (L553): `teardown_thing` paginates `list_thing_principals` rather than
consuming only the first page (8 principals). A Thing with >8 attached
certs (after multiple provision_robot reruns) would otherwise leak
orphaned certs on teardown.
- (L171): document why the `AllowResponseToAnyOperator` wildcard middle
segment is intentional (operator's thing-name is the recipient; trying
to scope tighter would force operators to know each robot's ThingName
to route responses, breaking the topic contract). Defence-in-depth via
the operator-side `AllowOwnSubscriptions` ACL keeps stray publishes
from being delivered to anyone.
* fix(mesh/iot): paginator fallback for FakeIoT mock compatibility
The `teardown_thing` paginator-aware fix from #553 broke 2 unit tests
using FakeIoT mocks that don't expose `get_paginator`. Made the impl
resilient with a hasattr-check fallback to single-call
`list_thing_principals`. Production paths still paginate.
This back-port matches the fix on pentest/full-stack so the cross-PR
merge is a clean no-op.
* fix(mesh/iot): tighten CA-unverified marker to owner-only + document except
Closes the two CodeQL findings on the .unverified sidecar marker block:
1. py/overly-permissive-file-permission (#273) on os.chmod(marker, 0o644).
The marker is a local sentinel read only by this process via
_ensure_ca; no other user needs read access. Switch to 0o600
(owner-only) and add the rationale inline.
2. py/empty-except (#274) on the surrounding 'except OSError: pass'.
Replace the silent pass with a logger.debug that records the
(best-effort) failure mode, matching the AGENTS.md narrow-except
discipline already followed elsewhere in this PR.
Pin regression test (test_iot_ca_pin.py::TestUnverifiedMarkerPermissions)
covers both axes:
* test_marker_written_owner_only_when_breakglass_active
asserts mode == 0o600 after _ensure_ca runs with
STRANDS_MESH_DISABLE_CA_PIN=true. Pre-fix code would write 0o644
and fail this assertion.
* test_marker_not_written_when_breakglass_inactive
pins the contract that the marker only appears under the
break-glass path -- guards against a future refactor that
promotes the sidecar to the canonical-CA branch.
109 tests pass locally across the iot mesh suite.
* style(tests/mesh): ruff format test_iot_ca_pin.py
Format-only fix to unblock CI. No semantic change.
`ruff format --check` was failing on tests/mesh/test_iot_ca_pin.py
(introduced in the R9 marker-permission test additions). Running
`ruff format` collapses three multi-line def signatures and one
multi-line assertion message into single lines that fit within the
120-char line-length budget configured in pyproject.toml.
Verified locally:
ruff check strands_robots tests tests_integ -> All checks passed
ruff format --check strands_robots tests tests_integ -> 204 files already formatted
* fix(mesh/iot): R10 review - regex fullmatch + numpy importorskip + CHANGELOG
Three concerns from review feedback (R10, 2026-06-02T11:35):
1. **Regex anchoring bug** (provision.py:352). ``_THING_NAME_RE.match()``
accepts a trailing newline because in non-MULTILINE mode ``$`` matches
*just before* a trailing ``\n``. Verified pre-fix:
``_validate_thing_name('robot\n')`` returned without raising. The PR
description for #228 explicitly claims the regex is "anchored, not
just `match`" -- this is description-vs-diff drift. Fix: switch to
``re.fullmatch`` (matches the existing posture of ``_PIN_RE.fullmatch``
on line 881). Five new pin tests in ``tests/mesh/test_iot_provision.py``
::TestValidateThingNameFullmatch cover trailing ``\n``/``\r``/``\t``/
form-feed and embedded ``\n``. All five FAIL on pre-fix code.
2. **Module-level numpy import** (test_iot_camera_offload.py:10).
``import numpy as np`` at module top makes the entire test file
skip-on-import-error if numpy is unavailable in the env -- pytest
surfaces this as a collection ERROR, not as a navigable skip. numpy
is only used by three frame-encoding tests at lines 441/483/507.
Switch to ``np = pytest.importorskip("numpy")``; rest of the suite
still runs. AGENTS.md > Review Learnings (#85): test import paths
must match production -- camera_offload itself does not require
numpy at module-import time.
3. **CHANGELOG.md entry** for #228 (was missing). Documents:
- Behaviour change: ``CameraOffloader.presign_ttl`` default cut
3600s -> 60s with explicit migration note for deployments that
need >60s of validity (env var or kwarg opt-in to 3600).
- The IoT hardening additions (CA pinning, thing-name regex,
policy scope, per-recv timeout, ``teardown_thing(cert_dir=)``).
- The three new env vars and the known follow-up issues
(#249/#251/#259/#260).
Deferred to follow-ups (per AGENTS.md round-budget; #228 PR
description R8/R9 bound declaration applies):
- #251 (chunked-read parity in _ensure_ca) -- already tracked.
- #259 (kwarg negative-TTL WARNING symmetry) -- already tracked.
- New issues for: marker create-then-chmod TOCTOU window (file-perm
race); symlink rejection clarity in _ensure_ca's "unreadable or
symlink" error; severity-field info leak on safety/event topic.
Local verification:
ruff check strands_robots/mesh/iot tests/mesh/test_iot_*.py -> All checks passed
pytest tests/mesh/test_iot_provision.py -> 34 passed (incl. 5 new pins)
Refs review feedback on PR #228 R10.
---------
Co-authored-by: strands-agent <217235299+strands-agent@users.noreply.github.com>
Co-authored-by: strands-robots-agent <agent@strands-robots.local>
Co-authored-by: strands-robots-bot <strands-robots-bot@users.noreply.github.com>
Co-authored-by: cagataycali <cagataycali@users.noreply.github.com>
Co-authored-by: strands-robots[bot] <strands-robots[bot]@users.noreply.github.com>
Co-authored-by: strands-robots-agent <agent@strands-robots.dev>
Co-authored-by: strands-agent <agent@strands.local>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: strands-coder[bot] <strands-coder[bot]@users.noreply.github.com>1 parent 4fe3af6 commit 0bfe11a
15 files changed
Lines changed: 2140 additions & 54 deletions
File tree
- strands_robots/mesh/iot
- tests/mesh
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
6 | 64 | | |
7 | 65 | | |
8 | 66 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
501 | 501 | | |
502 | 502 | | |
503 | 503 | | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
504 | 507 | | |
505 | 508 | | |
506 | 509 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
6 | | - | |
| 5 | + | |
| 6 | + | |
7 | 7 | | |
8 | | - | |
| 8 | + | |
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| |||
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
23 | | - | |
| 23 | + | |
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
30 | | - | |
| 30 | + | |
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
40 | | - | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
41 | 57 | | |
42 | 58 | | |
43 | 59 | | |
| |||
50 | 66 | | |
51 | 67 | | |
52 | 68 | | |
53 | | - | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
54 | 76 | | |
55 | 77 | | |
56 | 78 | | |
| |||
70 | 92 | | |
71 | 93 | | |
72 | 94 | | |
73 | | - | |
74 | | - | |
75 | | - | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
76 | 135 | | |
77 | 136 | | |
78 | 137 | | |
| |||
120 | 179 | | |
121 | 180 | | |
122 | 181 | | |
123 | | - | |
| 182 | + | |
124 | 183 | | |
125 | 184 | | |
126 | 185 | | |
| |||
130 | 189 | | |
131 | 190 | | |
132 | 191 | | |
133 | | - | |
| 192 | + | |
134 | 193 | | |
135 | 194 | | |
136 | 195 | | |
| |||
181 | 240 | | |
182 | 241 | | |
183 | 242 | | |
184 | | - | |
| 243 | + | |
185 | 244 | | |
186 | 245 | | |
187 | 246 | | |
| |||
195 | 254 | | |
196 | 255 | | |
197 | 256 | | |
198 | | - | |
| 257 | + | |
| 258 | + | |
199 | 259 | | |
200 | 260 | | |
201 | 261 | | |
202 | 262 | | |
203 | | - | |
204 | | - | |
| 263 | + | |
| 264 | + | |
205 | 265 | | |
206 | 266 | | |
207 | 267 | | |
| |||
231 | 291 | | |
232 | 292 | | |
233 | 293 | | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
234 | 303 | | |
235 | | - | |
| 304 | + | |
236 | 305 | | |
237 | 306 | | |
238 | 307 | | |
| |||
0 commit comments