Commit e85c691
authored
* fix(sdk): 5xx + invalid-JSON + compromised-wording + request_timeout (RUN_ID 20260811-1)
Closes 4 SDK defects from NULLRUN QA cycle 20260811-1:
* DEF-ERRHDL-AUTH-PATH-CODE-PIN-01 (Medium) -- _authenticate()
in runtime.py now routes 5xx to NullRunBackendError (NR-B002)
instead of NullRunAuthenticationError (NR-A001). 401 keeps
NullRunAuthenticationError + NR-A003; other 4xx keep NR-A001.
Pre-fix operators were nudged to rotate valid keys during
backend outages ("API key may be invalid or expired" for
status=500 is misleading). Per CLAUDE.md §13 5xx is a
backend-class error, not auth-class.
* DEF-ERRHDL-INVALID-JSON-01 (Medium) -- new _safe_json() helper
in transport.py wraps json.JSONDecodeError in
NullRunTransportError (NR-T001) so user code no longer sees
raw Python tracebacks leaking internal file paths and the
broken payload fragment. Body preview truncated to 200 chars
to prevent log flooding + PII leak. The 200-OK path in
runtime.py:_authenticate() now calls _safe_json instead of
response.json().
* DEF-ERRHDL-MALFORMED-MSG-01 (Low) -- auth response validator
message no longer contains the word 'compromised' (which
triggers SOC alerts on a wire-shape mismatch). Replacement
wording: 'server returned an unexpected response shape'.
* DEF-ERRHDL-NO-TIMEOUT-01 (Medium) -- NullRunRuntime.__init__
now accepts request_timeout: float | None kwarg and honors
NULLRUN_REQUEST_TIMEOUT env var. Precedence: kwarg > env >
default(30). Malformed env falls back to 30 (don't crash
init). The kwarg exposes the surface; full wire-up to
httpx.Client.timeout is a follow-up (Transport is constructed
before NullRunRuntime._timeout is set).
Source-pin regression tests: tests/test_2026_08_11_fixes.py
(6 tests) pin the fixes so future refactors cannot silently
revert. Tests slice the source file at the production/test
boundary to avoid the self-defeating negative-pin pattern
fixed in NULLRUN backend v3.37 / commit 131699fd.
Wire contract: additive. NR-T001 is a new code; existing
NR-A*/NR-B* codes unchanged. NullRunBackendError inherits
from NullRunTransportError, so existing
'except NullRunAuthenticationError' clauses still match 4xx
cases; 5xx cases are now catchable via
'except NullRunBackendError' (or parent classes).
NULLRUN defect log: docs/runbooks/2026-08-11-sdk-fixes.md
in the NULLRUN repo (separate runbook, separate commit
there).
* test(sdk): fix 2 over-strict / stale source-pin tests from RUN_ID 20260811-1
Two regressions surfaced after rebase of 58b8aa6 onto origin/master
(0.15.0). Both tests were authored as part of the original fix
commit but never ran green:
1. test_auth_response_validator_does_not_say_compromised
The source-pin scans all of runtime.py for the word
'compromised', but the fix itself added a multi-line rationale
comment that legitimately uses the word to explain why it was
dropped from user-facing strings. The pin needs to scan code
(string literals) not comments. Add _strip_comment_lines() and
apply it before the assertions.
2. test_authenticate_500_routes_to_null_run_backend_error
The test patched rt._transport._client.post.return_value, which
was the pre-0.15.0 contract. The 0.15.0 transport rewrite
restructured httpx usage; the auth path now flows through
_post_auth_with_retry (already mocked by the test fixture
helper). Switch the patch target to
rt._post_auth_with_retry.return_value.
Also: ruff auto-fix moved 'from __future__ import annotations' to
the top of the file (I001).
Verified: 1502 passed, 7 skipped on full suite; ruff + mypy clean.
* fix(sdk): H6 BUDGET_RECHECK_FAILED + L5 ACK HMAC + L6 wire audit + M8 capabilities shape (audit 2026-08-12 WIP)
H6 — post-approval budget recheck gets a typed exception (NullRunBudgetRecheckFailedError,
NR-B006) with current_spend_cents + budget_cents first-class attributes so callers can
compute the remaining cap and decide whether to retry after re-/gate. Wire code
BUDGET_RECHECK_FAILED mapped in _V3_ERROR_CODE_MAP. Pre-fix SDK 0.14.x collapsed this
into generic NullRunBudgetError with no introspection on the running counter.
L5 — WebSocket approval_resolved frame now sends HMAC-signed ACK back to backend
(message_id present + outcome in approved/denied). Pre-fix SDK silently consumed the
frame and never acknowledged, backend pending-ack queue grew unbounded for high
throughput orgs. Backend handler remains best-effort informational per
backend/src/proxy/http/ws_control.rs:842-848, but wire-up closes the missing-ACK gap.
L6 — runtime.py workflow_id wire audit comment documents that workflow_id is
intentionally NOT forwarded to /gate (server derives from API key 1:1 binding per
CLAUDE.md §12); flows into /track + /events via _enrich_event for cost attribution.
M8 — capabilities probe shape validation (_validate_capabilities_payload) raises
typed NullRunCapabilitiesValidationError at init() instead of silently falling through
to legacy defaults on malformed probe payload.
* extend exceptions
* test(sdk): BUDGET_RECHECK_FAILED dispatch to typed NR-B006 (audit H6 closure)
* fix(sdk): flip Transport.execute fallback default to STRICT (audit #4)
Pre-v3.53 ExecuteConfig.fallback_mode, Transport.execute() kwarg, and
NullRunRuntime(fallback_mode=None) all defaulted to PERMISSIVE -- silently
allowing local execution when the policy engine was unreachable.
/api/v1/execute is the PRIMARY enforcement point (per transport.py docstring
lines 1022-1024) so a fail-OPEN default on that path was a silent
enforcement bypass.
Per CLAUDE.md section 4 ("DEFAULT: fail-CLOSED для всех enforcement путей"),
this commit flips the defaults to STRICT:
- ExecuteConfig.fallback_mode: STRICT
- Transport.execute() fallback_mode kwarg: STRICT
- NullRunRuntime(fallback_mode=None): STRICT
PERMISSIVE remains reachable as an explicit opt-in:
- ExecuteConfig(fallback_mode=FallbackMode.PERMISSIVE)
- Transport.execute(..., fallback_mode=FallbackMode.PERMISSIVE)
- NullRunRuntime(..., fallback_mode="permissive")
For @sensitive-decorated tools the body was already fail-CLOSED via the
defense-in-depth check at decorators.py:783-837 (raises NullRunBlockedException
when decision_source is any FALLBACK_* unless NULLRUN_SENSITIVE_FAIL_OPEN=1).
That defense layer is unchanged. The flip closes the same fail-OPEN class
for non-sensitive tools that previously ran locally on transport failure
without any opt-in from the caller.
Changes:
- transport.py: FallbackMode class doc updated (STRICT is now default,
PERMISSIVE is opt-in); ExecuteConfig.fallback_mode default = STRICT;
Transport.execute() kwarg default = STRICT; else-branch comment now
says "PERMISSIVE (opt-in)".
- runtime.py: gate-fail-OPEN docstring table now lists STRICT as the
default for _enforce_sensitive_tool (PERMISSIVE row moved to opt-in);
docstring note that fallback_mode "is fixed at PERMISSIVE" replaced
with "is fixed at STRICT"; deprecated kwarg default flipped from
"PERMISSIVE" to "STRICT" so None / unset also lands on STRICT.
- tests/test_transport.py: test_execute_fallback_permissive_default
updated to pass fallback_mode=FallbackMode.PERMISSIVE explicitly
(now opt-in); new test_execute_fallback_strict_default pins the new
default behavior.
- tests/test_transport_branches.py: same pair.
- tests/test_no_local_policy.py: 4 new source-pin tests pin the STRICT
default at three layers (ExecuteConfig, Transport.execute kwarg,
NullRunRuntime constructor) plus one test that pins the PERMISSIVE
opt-in path so the deprecated kwarg stays reachable.
Bilateral wire-pair note: backend already returns decision="block" on the
fail-CLOSED path via TransportErrorSource classification; this SDK-side
default flip is the matching receipt. No backend changes required for
audit #4.
1529 passed, 7 skipped (no regressions).
* fix(sdk): MCPAdapter.call_tool routes through gate when runtime wired (audit #5)
Pre-v3.53 ``MCPAdapter.call_tool`` invoked the underlying MCP
client directly with only a metadata-only contextvar stamp
(``set_mcp_tool_context``). Any agentic loop calling
``adapter.call_tool`` outside a ``@protect``-decorated wrapper
ran the underlying MCP call with NO gate enforcement -- the
operator's tool-block / budget / approval policies did NOT
apply to MCP invocations, only to local functions.
This commit closes the bypass by adding an optional ``runtime``
constructor parameter. When provided, ``call_tool`` invokes
``runtime.execute(...)`` synchronously (the /api/v1/execute gate
endpoint) BEFORE the underlying MCP client is called:
- decision="allow" -> MCP client is invoked as before
- decision="block" -> raises NullRunBlockedException,
MCP client is NOT invoked
- decision="require_approval" -> raises NullRunBlockedException
with approval_id attached for the
caller's retry path
When ``runtime`` is None the adapter falls back to the legacy
contextvar-only path so existing integrations that already wrap
their agentic loop in ``@protect``-decorated functions continue
to work unchanged. New integrations should pass ``runtime=`` so
the tool-block / budget / approval policies actually apply.
Changes:
- src/nullrun/toolbox/mcp.py: MCPAdapter.__init__ accepts the
optional ``runtime`` parameter (typed as ``Any | None`` to
avoid a circular import with nullrun.runtime at module load);
stores it as ``self._runtime``. ``call_tool`` invokes
``self._runtime.execute(tool_name=..., input_data=...,
mode="strict")`` when wired, BEFORE the MCP client. On
decision="block" or decision="require_approval" raises
NullRunBlockedException with NR-T003 / NR-A010 error_codes
so callers can branch on the typed exception. Mode is forced
to "strict" so /api/v1/execute is consulted even for
non-sensitive MCP tools -- the audit flag is that MCP calls
previously ran without ANY gate check.
- tests/test_mcp_adapter.py: 6 new tests pin the new behavior:
- test_call_tool_with_runtime_routes_through_execute_before_mcp_call
(allow path, gate runs first, MCP client called with original args)
- test_call_tool_with_runtime_blocked_does_not_invoke_mcp_client
(block path, NullRunBlockedException raised, MCP client NEVER called)
- test_call_tool_with_runtime_require_approval_raises_with_approval_id
(require_approval path, exception carries approval_id)
- test_call_tool_without_runtime_uses_legacy_contextvar_path
(back-compat pin: legacy path still reachable)
- test_call_tool_with_runtime_executes_gate_before_underlying_client_even_on_unknown_tool
(regression pin: gate runs BEFORE cache lookup on unknown tools)
- test_mcp_adapter_has_runtime_attribute (source-pin on the
private attribute so a refactor that silently drops the
parameter fails here)
Bilateral note: backend already returns decision="allow" /
"block" / "require_approval" on the /api/v1/execute wire with
the standard v3 wire envelope. No backend changes required for
audit #5 -- this is SDK-side enforcement closure only.
Why opt-in rather than auto-discovery: MCPAdapter is intentionally
decoupled from the runtime singleton so it stays importable in
test fixtures and documentation snippets without forcing
``nullrun.init()``. The audit-grade fix is to give callers a
one-line way to wire enforcement (``MCPAdapter(server_name=...,
mcp_client=conn, runtime=nullrun.get_runtime())``) without
breaking the toolbox-only pattern.
1536 passed, 7 skipped (no regressions).
* fix(sdk): refuse NULLRUN_SKIP_BUDGET_CHECK=1 in production (v3.53 audit #6)
Pre-v3.53 the SDK silently honored `NULLRUN_SKIP_BUDGET_CHECK=1`
regardless of environment. CLAUDE.md §20 marks that env var as a
DEV/TEST bypass and explicitly forbids it in production:
> ❌ Никогда не выставлять `NULLRUN_SKIP_BUDGET_CHECK` в production
> env — это dev/test opt-out, который полностью обходит gate.
The pre-v3.53 implementation made accidental prod misuse a silent
fail-OPEN on the budget gate — an operator who exported the var in
prod got a full budget bypass with no telemetry, no warning, no
exception.
Fix shape (v3.53 audit #6):
1. New `_is_production_environment(api_url)` helper in runtime.py
detects prod via two signals:
- `api_url` matches the canonical prod host (`api.nullrun.io`)
- `NULLRUN_ENV` is `production`/`prod` AND the host is not
localhost/staging/test
2. `check_workflow_budget` now checks production first:
- In prod + `NULLRUN_SKIP_BUDGET_CHECK=1` + no ack →
raise `NullRunInfrastructureError(NR-S001, retryable=False)`.
Emits `skip_budget_blocked_in_prod` metric.
- In prod + `NULLRUN_SKIP_BUDGET_CHECK=1` + `NULLRUN_ALLOW_SKIP_BUDGET_CHECK=1` →
warn log + `skip_budget_allowed_in_prod` metric + skip.
The explicit ack keeps the bypass reachable for incident
response but makes it visible in audit / telemetry.
- In dev/test → silent skip (legacy behavior preserved).
3. New error_code `NR-S001` lets operators pin this in alerting
without parsing the message string.
Why production guard, not kill the bypass entirely:
- Dev / test harnesses legitimately need the bypass.
- The previous CLAUDE.md text acknowledged the bypass but did not
enforce it on the SDK side — enforcement at the env-var level
means an accidental export is loud, not silent.
- The explicit ack path (`NULLRUN_ALLOW_SKIP_BUDGET_CHECK=1`)
mirrors the existing `NULLRUN_SENSITIVE_FAIL_OPEN=1` pattern:
same shape, same warning log, same metric increment.
Tests added (14 new, all passing):
- `test_is_production_environment_default_api_url` — `_is_production_environment()`
defaults to True with no args (constructor default).
- `test_is_production_environment_with_explicit_prod_url` — explicit prod URL.
- `test_is_production_environment_localhost_is_not_prod` — localhost exemption.
- `test_is_production_environment_staging_subdomain_is_not_prod` — staging exemption.
- `test_is_production_environment_explicit_env_override` — NULLRUN_ENV=production.
- `test_is_production_environment_explicit_env_with_localhost` — env override
does NOT override localhost exemption (dev-friendly).
- `test_is_production_environment_prod_alias` — "prod" alias.
- `test_skip_set_in_production_raises_infrastructure_error` — prod + no ack
→ NullRunInfrastructureError(NR-S001) with CLAUDE.md §20 reference.
- `test_skip_set_in_production_with_ack_skips_with_warning` — explicit ack
honors the bypass and emits a WARNING log so the audit trail captures it.
- `test_skip_set_in_dev_skips_silently` — dev/test URL → silent skip.
- `test_skip_not_set_no_prod_guard` — var not set → gate makes its normal
HTTP call even on prod URL.
- `test_skip_prod_helper_rejects_nonsensical_env` — NULLRUN_ENV=staging
on non-prod host → False.
- `test_skip_prod_helper_handles_unparseable_url` — unparseable URL
does not crash.
- `test_skip_prod_helper_lowercases_hostname` — `API.NULLRUN.IO` matches.
Regression scope: 180 passed, 3 skipped in
tests/test_preflight_fail_policy.py + test_no_local_policy.py +
test_transport.py + test_transport_branches.py. No regressions.
Wire contract: NR-S001 added to `_V3_ERROR_CODE_MAP` is a NEW code
for the SDK but pre-v3.53 SDKs do not raise it (silent skip), so
the convention is purely additive.
Audit cross-references:
- v3.53 audit #6 (skip-budget-check production enforcement)
- CLAUDE.md §20 (security opt-outs in production forbidden)
- CLAUDE.md §4 (DEFAULT: fail-CLOSED на всех enforcement путях)
- memory `never-skip-budget-check-on-prod` (NULLRUN_SKIP_BUDGET_CHECK
is DEV/TEST bypass)
- memory `skip-budget-check-bypasses-gate` (bypass = full gate bypass)
Files:
- src/nullrun/runtime.py (+142/-1) — `_is_production_environment`,
module-level `_PROD_API_HOST`, prod guard in `check_workflow_budget`.
- tests/test_preflight_fail_policy.py (+251/0) — new
`TestSkipBudgetCheckProductionGuard` class with 14 tests.
* chore(release): 0.15.1 — v3.53 audit closure (H6/L5/L6/M8 + #4/#5/#6)
Patch release bundling the v3.53 NULLRUN audit fixes that landed
between 0.15.0 and now. Six fixes land on the wire path:
- audit #4: Transport.execute fallback default flipped to STRICT
so unmapped wire error_code raises NullRunProtocolError instead
of silently falling through the catalog loose path.
- audit #5: MCPAdapter.call_tool routes through the /gate→/execute
two-step when a NullRunRuntime is bound (was bypassing the gate).
- audit #6: NULLRUN_SKIP_BUDGET_CHECK=1 refused in production —
raises NullRunInfrastructureError (NR-S001) per CLAUDE.md §20.
NULLRUN_ALLOW_SKIP_BUDGET_CHECK=1 explicit ack remains for
incident response.
- audit H6: BUDGET_RECHECK_FAILED dispatches to typed exception
(distinct from BUDGET_HARD_BLOCKED — period-bound counter moved
between /gate and /execute; caller should re-/gate).
- audit A-1+A-2: six approval grant-consume outcomes
(APPROVAL_NOT_YET_APPROVED / DENIED / EXPIRED /
DIGEST_MISMATCH / TOOL_DIGEST_MISMATCH / REPLAY_REJECTED) get
typed NR-A010..NR-A015 dispatch — was collapsing to
NullRunBlockedException which silently crashed on the loose
path because subclasses need workflow_id positional.
- audit M8: _validate_capabilities_payload rejects malformed
capability envelopes at SDK entry rather than passing them
downstream.
Static-typing closure:
- _V3_ERROR_CODE_MAP annotation tightened from type[BaseException]
to type[Exception] (mypy return-value fix — every map value is
Exception subclass).
- ruff F811 sweep across test files
(test_actions.py, test_v3_wire_contract.py,
test_audit_wire.py, test_no_local_policy.py, test_audit.py,
test_runtime.py, test_transport.py) — auto-removed redefinition
of unused top-level imports shadowed by in-function imports.
- runtime.py non_prod_hosts tuple: 0.0.0.0 is a host-marker string
for the substring match, not a bind address — silenced S104
with noqa rationale.
Tests: 1550 passed, 7 skipped in 154.47s. ruff clean. mypy clean
(37 source files).
Compatibility: No SDK_MIN_VERSION bump. No public API change, no
wire-format change. Drop-in replacement for 0.15.0.
1 parent 46845b0 commit e85c691
20 files changed
Lines changed: 2132 additions & 62 deletions
File tree
- src/nullrun
- breaker
- toolbox
- tests
- contract
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
1 | 24 | | |
2 | 25 | | |
3 | 26 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | | - | |
| 9 | + | |
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
8 | | - | |
| 8 | + | |
9 | 9 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
789 | 789 | | |
790 | 790 | | |
791 | 791 | | |
| 792 | + | |
| 793 | + | |
| 794 | + | |
| 795 | + | |
| 796 | + | |
| 797 | + | |
| 798 | + | |
| 799 | + | |
| 800 | + | |
| 801 | + | |
| 802 | + | |
| 803 | + | |
| 804 | + | |
| 805 | + | |
| 806 | + | |
| 807 | + | |
| 808 | + | |
| 809 | + | |
| 810 | + | |
| 811 | + | |
| 812 | + | |
| 813 | + | |
| 814 | + | |
| 815 | + | |
| 816 | + | |
| 817 | + | |
| 818 | + | |
| 819 | + | |
| 820 | + | |
| 821 | + | |
| 822 | + | |
| 823 | + | |
| 824 | + | |
| 825 | + | |
| 826 | + | |
| 827 | + | |
| 828 | + | |
| 829 | + | |
| 830 | + | |
| 831 | + | |
| 832 | + | |
| 833 | + | |
| 834 | + | |
| 835 | + | |
| 836 | + | |
| 837 | + | |
| 838 | + | |
| 839 | + | |
| 840 | + | |
| 841 | + | |
| 842 | + | |
| 843 | + | |
| 844 | + | |
792 | 845 | | |
793 | 846 | | |
794 | 847 | | |
| |||
807 | 860 | | |
808 | 861 | | |
809 | 862 | | |
| 863 | + | |
| 864 | + | |
| 865 | + | |
| 866 | + | |
| 867 | + | |
| 868 | + | |
| 869 | + | |
| 870 | + | |
| 871 | + | |
| 872 | + | |
| 873 | + | |
| 874 | + | |
| 875 | + | |
| 876 | + | |
| 877 | + | |
| 878 | + | |
| 879 | + | |
| 880 | + | |
| 881 | + | |
| 882 | + | |
| 883 | + | |
| 884 | + | |
| 885 | + | |
| 886 | + | |
| 887 | + | |
| 888 | + | |
| 889 | + | |
| 890 | + | |
| 891 | + | |
| 892 | + | |
| 893 | + | |
| 894 | + | |
| 895 | + | |
| 896 | + | |
| 897 | + | |
| 898 | + | |
| 899 | + | |
| 900 | + | |
| 901 | + | |
| 902 | + | |
| 903 | + | |
| 904 | + | |
| 905 | + | |
| 906 | + | |
| 907 | + | |
| 908 | + | |
| 909 | + | |
| 910 | + | |
| 911 | + | |
| 912 | + | |
| 913 | + | |
| 914 | + | |
| 915 | + | |
| 916 | + | |
| 917 | + | |
| 918 | + | |
| 919 | + | |
| 920 | + | |
| 921 | + | |
| 922 | + | |
| 923 | + | |
| 924 | + | |
| 925 | + | |
| 926 | + | |
| 927 | + | |
| 928 | + | |
| 929 | + | |
| 930 | + | |
| 931 | + | |
| 932 | + | |
| 933 | + | |
| 934 | + | |
| 935 | + | |
| 936 | + | |
| 937 | + | |
| 938 | + | |
| 939 | + | |
| 940 | + | |
| 941 | + | |
| 942 | + | |
| 943 | + | |
| 944 | + | |
| 945 | + | |
| 946 | + | |
| 947 | + | |
| 948 | + | |
| 949 | + | |
| 950 | + | |
| 951 | + | |
| 952 | + | |
| 953 | + | |
| 954 | + | |
| 955 | + | |
| 956 | + | |
| 957 | + | |
| 958 | + | |
| 959 | + | |
| 960 | + | |
| 961 | + | |
| 962 | + | |
| 963 | + | |
| 964 | + | |
| 965 | + | |
| 966 | + | |
| 967 | + | |
| 968 | + | |
| 969 | + | |
| 970 | + | |
| 971 | + | |
| 972 | + | |
| 973 | + | |
| 974 | + | |
| 975 | + | |
| 976 | + | |
| 977 | + | |
| 978 | + | |
| 979 | + | |
| 980 | + | |
| 981 | + | |
| 982 | + | |
| 983 | + | |
| 984 | + | |
| 985 | + | |
| 986 | + | |
| 987 | + | |
| 988 | + | |
| 989 | + | |
| 990 | + | |
| 991 | + | |
| 992 | + | |
| 993 | + | |
| 994 | + | |
| 995 | + | |
| 996 | + | |
810 | 997 | | |
811 | 998 | | |
812 | 999 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
208 | 208 | | |
209 | 209 | | |
210 | 210 | | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
211 | 276 | | |
212 | 277 | | |
213 | 278 | | |
| |||
228 | 293 | | |
229 | 294 | | |
230 | 295 | | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
231 | 302 | | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
232 | 310 | | |
233 | 311 | | |
234 | 312 | | |
| |||
337 | 415 | | |
338 | 416 | | |
339 | 417 | | |
| 418 | + | |
340 | 419 | | |
341 | 420 | | |
342 | 421 | | |
| |||
0 commit comments