bdo-toolkit 1.0.4__tar.gz → 1.0.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bdo_toolkit-1.0.4/src/bdo_toolkit.egg-info → bdo_toolkit-1.0.6}/PKG-INFO +12 -1
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/README.md +11 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/__init__.py +7 -1
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_calibration/_records.py +36 -1
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_item_state/inventory.py +4 -4
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_version.py +1 -1
- bdo_toolkit-1.0.6/src/bdo_toolkit/agris/__init__.py +16 -0
- bdo_toolkit-1.0.6/src/bdo_toolkit/agris/_capture.py +87 -0
- bdo_toolkit-1.0.6/src/bdo_toolkit/agris/_discovery.py +287 -0
- bdo_toolkit-1.0.6/src/bdo_toolkit/agris/_validation.py +13 -0
- bdo_toolkit-1.0.6/src/bdo_toolkit/agris/async_session.py +295 -0
- bdo_toolkit-1.0.6/src/bdo_toolkit/agris/models.py +134 -0
- bdo_toolkit-1.0.6/src/bdo_toolkit/agris/replay.py +112 -0
- bdo_toolkit-1.0.6/src/bdo_toolkit/agris/session.py +386 -0
- bdo_toolkit-1.0.6/src/bdo_toolkit/capture_diagnosis.py +246 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/cli.py +185 -1
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6/src/bdo_toolkit.egg-info}/PKG-INFO +12 -1
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit.egg-info/SOURCES.txt +17 -0
- bdo_toolkit-1.0.6/tests/test_agris_async.py +472 -0
- bdo_toolkit-1.0.6/tests/test_agris_cli.py +124 -0
- bdo_toolkit-1.0.6/tests/test_agris_discovery.py +382 -0
- bdo_toolkit-1.0.6/tests/test_agris_evidence.py +51 -0
- bdo_toolkit-1.0.6/tests/test_agris_models.py +98 -0
- bdo_toolkit-1.0.6/tests/test_agris_replay.py +81 -0
- bdo_toolkit-1.0.6/tests/test_agris_session.py +305 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_calibration_progress.py +6 -1
- bdo_toolkit-1.0.6/tests/test_capture_diagnosis.py +215 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_character_state.py +95 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_direction.py +91 -1
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/LICENSE +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/pyproject.toml +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/setup.cfg +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_async_sessions.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_async_utils.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_calibration/__init__.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_calibration/_constants.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_calibration/_formatting.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_calibration/analysis.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_calibration/capture.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_calibration/companions.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_calibration/live.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_calibration/models.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_calibration/observations.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_calibration/persistence.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_calibration/progress.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_calibration/validation.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_calibration/workflow.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_capture_backend.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_capture_options.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_capture_runtime.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_deposit_origin.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_engine.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_framing.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_item_state/__init__.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_item_state/_constants.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_item_state/_records.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_item_state/assembly.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_item_state/formatting.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_item_state/models.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_item_state/session.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_item_state/storage.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_origin/__init__.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_origin/discovery.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_origin/manual.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_origin/models.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_origin/tracker.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_profile_io.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_profile_runtime.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_protocol.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_reassembly.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_record_geometry.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_specs.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_storage_destination_validation.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/_storage_hydration.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/calibration.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/capture.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/character_state.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/diagnostics.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/events.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/filters.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/item_state.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/origin_learning.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/profiles.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/py.typed +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/remote_profiles.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/solare/__init__.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/solare/_constants.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/solare/_detail_learning.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/solare/_details.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/solare/_discovery.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/solare/_live_tracker.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/solare/_replay_capture.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/solare/_result.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/solare/_scanner.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/solare/_validation.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/solare/async_session.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/solare/models.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/solare/replay.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/solare/session.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit/writers.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit.egg-info/dependency_links.txt +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit.egg-info/entry_points.txt +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit.egg-info/requires.txt +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/src/bdo_toolkit.egg-info/top_level.txt +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_async_sessions.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_calibration.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_calibration_conveniences.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_calibration_lifecycle.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_calibration_persistence.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_calibration_retention.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_capture_backend.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_capture_replay.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_capture_runtime.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_cli.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_decoder_diagnostics.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_deposit_origin.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_dynamic_storage_calibration_matrix.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_engine.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_examples_and_writers.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_explicit_profile.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_fixture_catalog.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_fixture_regressions.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_framing.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_initial_load_storage.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_inventory_snapshots.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_item_state.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_live_capture_session.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_origin_learning.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_package_layout.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_profile_validation.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_reassembly.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_remote_profiles.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_solare_api.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_solare_details.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_solare_learning.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_solare_learning_adversarial.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_solare_real_captures.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_solare_replay.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_solare_session.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_storage_destination_validation.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_storage_events.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_storage_hydration_tracker.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_storage_unknown_destination_calibration.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.6}/tests/test_toolkit_api.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bdo-toolkit
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.6
|
|
4
4
|
Summary: Passive, read-only BDO packet parsing toolkit for app developers.
|
|
5
5
|
Author: bdo-toolkit contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -47,6 +47,17 @@ traffic into structured, application-ready data.
|
|
|
47
47
|
> captures. It does not send or modify packets, replay traffic to the game,
|
|
48
48
|
> automate gameplay, inspect process memory, or bypass anti-cheat software.
|
|
49
49
|
|
|
50
|
+
## Live-capture limitations
|
|
51
|
+
|
|
52
|
+
Live capture depends heavily on your network setup and on whether the capture
|
|
53
|
+
backend can see BDO traffic. VPNs and routing services such as ExitLag may require
|
|
54
|
+
manual interface, local IP, and port settings; automatic detection may select the
|
|
55
|
+
wrong connection. Traffic visible only as an encrypted tunnel cannot be decoded.
|
|
56
|
+
Follow [Diagnose capture when automatic selection fails](https://ychwu.github.io/bdo-toolkit/#capture-foundation/diagnose-network)
|
|
57
|
+
to inspect connections on Windows, select grouped capture settings, and verify
|
|
58
|
+
them with game activity. The guide covers VPNs, local proxies, and cases where
|
|
59
|
+
no usable adapter is found. Discovery does not apply settings or verify decoding.
|
|
60
|
+
|
|
50
61
|
## Capabilities
|
|
51
62
|
|
|
52
63
|
bdo-toolkit exposes three passive workflows. Each can observe live traffic or
|
|
@@ -20,6 +20,17 @@ traffic into structured, application-ready data.
|
|
|
20
20
|
> captures. It does not send or modify packets, replay traffic to the game,
|
|
21
21
|
> automate gameplay, inspect process memory, or bypass anti-cheat software.
|
|
22
22
|
|
|
23
|
+
## Live-capture limitations
|
|
24
|
+
|
|
25
|
+
Live capture depends heavily on your network setup and on whether the capture
|
|
26
|
+
backend can see BDO traffic. VPNs and routing services such as ExitLag may require
|
|
27
|
+
manual interface, local IP, and port settings; automatic detection may select the
|
|
28
|
+
wrong connection. Traffic visible only as an encrypted tunnel cannot be decoded.
|
|
29
|
+
Follow [Diagnose capture when automatic selection fails](https://ychwu.github.io/bdo-toolkit/#capture-foundation/diagnose-network)
|
|
30
|
+
to inspect connections on Windows, select grouped capture settings, and verify
|
|
31
|
+
them with game activity. The guide covers VPNs, local proxies, and cases where
|
|
32
|
+
no usable adapter is found. Discovery does not apply settings or verify decoding.
|
|
33
|
+
|
|
23
34
|
## Capabilities
|
|
24
35
|
|
|
25
36
|
bdo-toolkit exposes three passive workflows. Each can observe live traffic or
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from ._async_sessions import AsyncCalibrationSession, AsyncLiveCaptureSession
|
|
4
4
|
from ._capture_options import LiveCaptureOptions, PacketCaptureOptions
|
|
5
5
|
from ._capture_runtime import CaptureEndpoint
|
|
6
|
+
from .capture_diagnosis import CaptureCandidate, CaptureDiagnosis, CaptureSettingsProposal, diagnose_capture
|
|
6
7
|
from .capture import (
|
|
7
8
|
CaptureIntegrityError,
|
|
8
9
|
LiveCaptureHealth,
|
|
@@ -40,7 +41,7 @@ from .origin_learning import (
|
|
|
40
41
|
promote_origin_candidates,
|
|
41
42
|
)
|
|
42
43
|
from .writers import ConsoleEventWriter, JsonlEventWriter
|
|
43
|
-
from . import solare
|
|
44
|
+
from . import agris, solare
|
|
44
45
|
from ._version import __version__
|
|
45
46
|
|
|
46
47
|
__all__ = [
|
|
@@ -48,6 +49,10 @@ __all__ = [
|
|
|
48
49
|
"AsyncLiveCaptureSession",
|
|
49
50
|
"BDOEvent",
|
|
50
51
|
"CaptureEndpoint",
|
|
52
|
+
"CaptureCandidate",
|
|
53
|
+
"CaptureDiagnosis",
|
|
54
|
+
"CaptureSettingsProposal",
|
|
55
|
+
"diagnose_capture",
|
|
51
56
|
"CaptureIntegrityError",
|
|
52
57
|
"ConsoleEventWriter",
|
|
53
58
|
"DecoderDiagnostic",
|
|
@@ -78,6 +83,7 @@ __all__ = [
|
|
|
78
83
|
"StorageLocation",
|
|
79
84
|
"__version__",
|
|
80
85
|
"capture_live",
|
|
86
|
+
"agris",
|
|
81
87
|
"fetch_opcode_profile",
|
|
82
88
|
"load_opcode_profile",
|
|
83
89
|
"promote_origin_candidates",
|
|
@@ -508,10 +508,45 @@ def _discover_storage_context_offset(
|
|
|
508
508
|
before_offset=before_offset,
|
|
509
509
|
)
|
|
510
510
|
if len(candidates) == 1:
|
|
511
|
-
|
|
511
|
+
offset = candidates[0][0]
|
|
512
|
+
if _storage_candidate_aliases_batch_count(frame, before_offset, offset):
|
|
513
|
+
return None
|
|
514
|
+
return offset
|
|
512
515
|
return None
|
|
513
516
|
|
|
514
517
|
|
|
518
|
+
def _storage_candidate_aliases_batch_count(
|
|
519
|
+
frame: BDOFrame, item_offset: int, destination_offset: int,
|
|
520
|
+
) -> bool:
|
|
521
|
+
"""A batch count cannot also provide independent destination evidence.
|
|
522
|
+
|
|
523
|
+
Require complete, uniformly spaced records and a plausible normalized
|
|
524
|
+
envelope. This only discounts a single-frame town candidate when every
|
|
525
|
+
geometrically possible count column overlaps it. A separate count column
|
|
526
|
+
preserves the destination signal (and any intrinsic direction conflict).
|
|
527
|
+
Cross-frame destination/count authority is deliberately unchanged.
|
|
528
|
+
"""
|
|
529
|
+
if frame.length != len(frame.message):
|
|
530
|
+
return False
|
|
531
|
+
offsets = _full_transfer_record_offsets(frame, item_offset, item_offset + 35)
|
|
532
|
+
stride = uniform_stride(offsets) if len(offsets) > 1 else None
|
|
533
|
+
if stride is None or item_offset not in offsets:
|
|
534
|
+
return False
|
|
535
|
+
count = len(offsets)
|
|
536
|
+
base_length = frame.length - (count - 1) * stride
|
|
537
|
+
prefix_length = frame.length - count * stride
|
|
538
|
+
if not (200 <= base_length <= 300 and 5 <= prefix_length <= offsets[0]):
|
|
539
|
+
return False
|
|
540
|
+
count_offsets = [
|
|
541
|
+
offset for offset in range(5, prefix_length - 1)
|
|
542
|
+
if int.from_bytes(frame.message[offset:offset + 2], "little") == count
|
|
543
|
+
]
|
|
544
|
+
return bool(count_offsets) and all(
|
|
545
|
+
_ranges_overlap(offset, 2, destination_offset, 4)
|
|
546
|
+
for offset in count_offsets
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
|
|
515
550
|
def _discover_storage_context_offset_from_frames(
|
|
516
551
|
frames: Iterable[BDOFrame],
|
|
517
552
|
*,
|
|
@@ -185,17 +185,17 @@ def _discover_inventory_tail_layout(
|
|
|
185
185
|
observed_codes: set[int] = set()
|
|
186
186
|
valid = True
|
|
187
187
|
for frame, group, _, _ in frame_groups:
|
|
188
|
-
codes =
|
|
188
|
+
codes = [
|
|
189
189
|
frame.message[int(event.record_offset) + container_relative]
|
|
190
190
|
for event in group
|
|
191
191
|
if event.record_offset is not None
|
|
192
192
|
and int(event.record_offset) + container_relative
|
|
193
193
|
< len(frame.message)
|
|
194
|
-
|
|
195
|
-
if len(codes) != 1:
|
|
194
|
+
]
|
|
195
|
+
if len(codes) != len(group) or len(set(codes)) != 1:
|
|
196
196
|
valid = False
|
|
197
197
|
break
|
|
198
|
-
code =
|
|
198
|
+
code = codes[0]
|
|
199
199
|
if code not in _INVENTORY_CONTAINER_LABELS:
|
|
200
200
|
valid = False
|
|
201
201
|
break
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Experimental passive Agris balances with required-cap cold discovery.
|
|
2
|
+
|
|
3
|
+
No item profile, persisted layout, starting balance, or consumption calculation
|
|
4
|
+
is used. A uniquely matching inferred layout is not universal semantic proof.
|
|
5
|
+
"""
|
|
6
|
+
from ._capture import AgrisCaptureHealth
|
|
7
|
+
from .async_session import AsyncLiveAgrisSession
|
|
8
|
+
from .models import AgrisBalance, AgrisDetectionError, AgrisDiscoveryOptions, AgrisLayout, AgrisStatus
|
|
9
|
+
from .replay import AgrisReplay, replay_agris
|
|
10
|
+
from .session import LiveAgrisSession
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"AgrisBalance", "AgrisCaptureHealth", "AgrisDetectionError", "AgrisDiscoveryOptions",
|
|
14
|
+
"AgrisLayout", "AgrisReplay", "AgrisStatus", "AsyncLiveAgrisSession",
|
|
15
|
+
"LiveAgrisSession", "replay_agris",
|
|
16
|
+
]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Shared Agris transport adapter and immutable capture diagnostics."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import asdict, dataclass
|
|
5
|
+
from typing import Iterable
|
|
6
|
+
|
|
7
|
+
from .._capture_backend import validate_server_ports
|
|
8
|
+
from .._framing import FrameCollectorScanner
|
|
9
|
+
from .._protocol import FlowKey
|
|
10
|
+
from .._reassembly import FlowManager
|
|
11
|
+
from ._discovery import AgrisTracker
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class AgrisCaptureHealth:
|
|
16
|
+
"""Loss indicators; unavailable native counters remain None, not zero."""
|
|
17
|
+
|
|
18
|
+
packets_processed: int = 0
|
|
19
|
+
packets_accepted: int = 0
|
|
20
|
+
packet_queue_overflows: int = 0
|
|
21
|
+
event_queue_overflows: int = 0
|
|
22
|
+
tcp_gap_resets: int = 0
|
|
23
|
+
flow_state_evictions: int = 0
|
|
24
|
+
pcap_received: int | None = None
|
|
25
|
+
pcap_dropped: int | None = None
|
|
26
|
+
pcap_interface_dropped: int | None = None
|
|
27
|
+
capture_buffer_bytes: int | None = None
|
|
28
|
+
cleanup_incomplete: bool = False
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def capture_is_clean(self) -> bool:
|
|
32
|
+
"""No known loss; unknown kernel counters are not proof of completeness."""
|
|
33
|
+
return not (self.packet_queue_overflows or self.event_queue_overflows
|
|
34
|
+
or self.tcp_gap_resets or self.flow_state_evictions
|
|
35
|
+
or self.pcap_dropped or self.pcap_interface_dropped)
|
|
36
|
+
|
|
37
|
+
def to_dict(self) -> dict[str, object]:
|
|
38
|
+
return {**asdict(self), "capture_is_clean": self.capture_is_clean}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AgrisFlowManager(FlowManager):
|
|
42
|
+
"""Generic framing, with no opcode hints or item-profile dependency."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, tracker: AgrisTracker, ports: Iterable[int], *, live: bool = False):
|
|
45
|
+
self.tracker = tracker
|
|
46
|
+
self.finishing = False
|
|
47
|
+
self.clock = 0.0
|
|
48
|
+
self.evictions = 0
|
|
49
|
+
super().__init__(server_ports=validate_server_ports(ports),
|
|
50
|
+
scanner_factory=lambda: FrameCollectorScanner(tracker.observe),
|
|
51
|
+
max_flows=64, max_pending_bytes=4 * 1024 * 1024,
|
|
52
|
+
track_flow_generations=True, defer_gap_timeouts=live,
|
|
53
|
+
on_flow_reset=tracker.gap_reset, on_flow_close=self._closed,
|
|
54
|
+
on_flow_eviction=self._evicted)
|
|
55
|
+
|
|
56
|
+
def _closed(self, flow: FlowKey) -> None:
|
|
57
|
+
if not self.finishing:
|
|
58
|
+
self.tracker.close_flow(flow)
|
|
59
|
+
|
|
60
|
+
def _evicted(self) -> None:
|
|
61
|
+
self.evictions += 1
|
|
62
|
+
self.tracker.invalidate("Agris flow-state limit reached; discovery is incomplete")
|
|
63
|
+
|
|
64
|
+
def process_tcp_segment(self, *, source_ip: str, source_port: int,
|
|
65
|
+
destination_ip: str, destination_port: int, sequence: int,
|
|
66
|
+
payload: bytes, timestamp: float, syn: bool = False,
|
|
67
|
+
rst: bool = False, fin: bool = False) -> None:
|
|
68
|
+
super().process_tcp_segment(source_ip=source_ip, source_port=source_port,
|
|
69
|
+
destination_ip=destination_ip, destination_port=destination_port,
|
|
70
|
+
sequence=sequence, payload=payload, timestamp=timestamp,
|
|
71
|
+
syn=syn, rst=rst, fin=fin)
|
|
72
|
+
if source_port in self.server_ports:
|
|
73
|
+
self.clock = max(self.clock, timestamp)
|
|
74
|
+
|
|
75
|
+
def refresh(self, now: float | None = None) -> None:
|
|
76
|
+
self.clock = max(self.clock, now if now is not None else self.clock)
|
|
77
|
+
self.tracker.refresh(self.clock)
|
|
78
|
+
|
|
79
|
+
def finish(self) -> None:
|
|
80
|
+
# EOF is not a network disconnect. Preserve the final observation, but
|
|
81
|
+
# do not forgive actual gaps exposed by draining buffered segments.
|
|
82
|
+
self.finishing = True
|
|
83
|
+
try:
|
|
84
|
+
super().finish()
|
|
85
|
+
self.refresh()
|
|
86
|
+
finally:
|
|
87
|
+
self.finishing = False
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""Bounded cold discovery, separate from selected-layout balance decoding.
|
|
2
|
+
|
|
3
|
+
No remembered opcode, message size, field offset, or profile participates in
|
|
4
|
+
identity. A unique capped decreasing uint32LE column remains an inference,
|
|
5
|
+
not proof that an unrelated resource cannot share those characteristics.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import math
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import NoReturn
|
|
14
|
+
|
|
15
|
+
from .._protocol import BDOFrame, FlowKey
|
|
16
|
+
from ..events import Flow
|
|
17
|
+
from ._validation import validate_expected_maximum_points
|
|
18
|
+
from .models import (
|
|
19
|
+
AgrisBalance,
|
|
20
|
+
AgrisDetectionError,
|
|
21
|
+
AgrisDiscoveryOptions,
|
|
22
|
+
AgrisDiscoveryState,
|
|
23
|
+
AgrisLayout,
|
|
24
|
+
AgrisStatus,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class _Column:
|
|
30
|
+
last: int
|
|
31
|
+
distinct: int = 1
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class _Family:
|
|
36
|
+
caps: set[int]
|
|
37
|
+
columns: dict[int, _Column]
|
|
38
|
+
observed_at: float
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
type _FamilyKey = tuple[FlowKey, int, int, int]
|
|
42
|
+
type _CandidateKey = tuple[_FamilyKey, int, int]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _public_flow(flow: FlowKey) -> Flow:
|
|
46
|
+
return Flow(flow.source_ip, flow.source_port, flow.destination_ip, flow.destination_port)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AgrisTracker:
|
|
50
|
+
"""Single-owner tracker; no raw frame history or consumption arithmetic.
|
|
51
|
+
|
|
52
|
+
Before selection, a family's surviving columns must be nonincreasing and
|
|
53
|
+
in range from its first observation. Contradicted hypotheses never restart
|
|
54
|
+
midway through that family. After selection, direct decoding permits equal
|
|
55
|
+
and increasing balances while discovery still checks competing hypotheses.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
*,
|
|
61
|
+
expected_maximum_points: int,
|
|
62
|
+
options: AgrisDiscoveryOptions | None = None,
|
|
63
|
+
on_balance: Callable[[AgrisBalance], None] | None = None,
|
|
64
|
+
) -> None:
|
|
65
|
+
self.expected_maximum_points = validate_expected_maximum_points(expected_maximum_points)
|
|
66
|
+
if options is not None and not isinstance(options, AgrisDiscoveryOptions):
|
|
67
|
+
raise TypeError("options must be AgrisDiscoveryOptions or None")
|
|
68
|
+
if on_balance is not None and not callable(on_balance):
|
|
69
|
+
raise TypeError("on_balance must be callable or None")
|
|
70
|
+
self.options = options if options is not None else AgrisDiscoveryOptions()
|
|
71
|
+
self._on_balance = on_balance
|
|
72
|
+
self._families: dict[_FamilyKey, _Family] = {}
|
|
73
|
+
self._column_count = 0
|
|
74
|
+
self._observed_frames = 0
|
|
75
|
+
self._clock = 0.0
|
|
76
|
+
self._selected: _CandidateKey | None = None
|
|
77
|
+
self._unique: _CandidateKey | None = None
|
|
78
|
+
self._unique_since = 0.0
|
|
79
|
+
self._candidates: tuple[_CandidateKey, ...] = ()
|
|
80
|
+
self._candidates_dirty = False
|
|
81
|
+
self._invalid_reason: str | None = None
|
|
82
|
+
self._latest_balance: AgrisBalance | None = None
|
|
83
|
+
self._selected_version = 0
|
|
84
|
+
self._published_version = -1
|
|
85
|
+
self._tracking = False
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def clock(self) -> float:
|
|
89
|
+
"""Latest settling clock, distinct from a balance's packet timestamp."""
|
|
90
|
+
|
|
91
|
+
return self._clock
|
|
92
|
+
|
|
93
|
+
def invalidate(self, reason: str) -> NoReturn:
|
|
94
|
+
"""Latch the first integrity failure and withdraw current authority."""
|
|
95
|
+
|
|
96
|
+
if self._invalid_reason is None:
|
|
97
|
+
self._invalid_reason = reason
|
|
98
|
+
self._tracking = False
|
|
99
|
+
raise AgrisDetectionError(self._invalid_reason)
|
|
100
|
+
|
|
101
|
+
def observe(self, frame: BDOFrame) -> None:
|
|
102
|
+
if self._invalid_reason is not None:
|
|
103
|
+
raise AgrisDetectionError(self._invalid_reason)
|
|
104
|
+
if not math.isfinite(frame.context.timestamp):
|
|
105
|
+
self.invalidate("A frame has a non-finite observation timestamp.")
|
|
106
|
+
self._observed_frames += 1
|
|
107
|
+
self._clock = max(self._clock, frame.context.timestamp)
|
|
108
|
+
message = frame.message
|
|
109
|
+
if self._selected is not None:
|
|
110
|
+
chosen = self._selected[0]
|
|
111
|
+
if frame.context.flow == chosen[0] and frame.context.flow_generation != chosen[1]:
|
|
112
|
+
self.invalidate("Selected connection epoch changed; restart discovery.")
|
|
113
|
+
if len(message) < 5:
|
|
114
|
+
self.refresh()
|
|
115
|
+
return
|
|
116
|
+
key = (frame.context.flow, frame.context.flow_generation, frame.opcode, len(message))
|
|
117
|
+
if self._selected is not None:
|
|
118
|
+
chosen = self._selected[0]
|
|
119
|
+
if key[:3] == chosen[:3]:
|
|
120
|
+
if key != chosen or frame.flag != 0 or frame.length != len(message):
|
|
121
|
+
self.invalidate("Selected message shape changed; restart discovery.")
|
|
122
|
+
self._decode_selected(frame)
|
|
123
|
+
if frame.flag != 0 or frame.length != len(message):
|
|
124
|
+
self.refresh()
|
|
125
|
+
return
|
|
126
|
+
if key not in self._families:
|
|
127
|
+
self._add_family(key, message, frame.context.timestamp)
|
|
128
|
+
else:
|
|
129
|
+
self._update_family(key, message, frame.context.timestamp)
|
|
130
|
+
self.refresh()
|
|
131
|
+
|
|
132
|
+
def _add_family(self, key: _FamilyKey, message: bytes, observed_at: float) -> None:
|
|
133
|
+
if len(self._families) >= self.options.max_families:
|
|
134
|
+
self.invalidate("Discovery family limit reached; no hypotheses were silently evicted.")
|
|
135
|
+
cap_bytes = self.expected_maximum_points.to_bytes(4, "little")
|
|
136
|
+
caps: set[int] = set()
|
|
137
|
+
offset = message.find(cap_bytes, 5)
|
|
138
|
+
while offset != -1:
|
|
139
|
+
caps.add(offset)
|
|
140
|
+
offset = message.find(cap_bytes, offset + 1)
|
|
141
|
+
columns: dict[int, _Column] = {}
|
|
142
|
+
if caps:
|
|
143
|
+
for offset in range(5, len(message) - 3):
|
|
144
|
+
value = int.from_bytes(message[offset : offset + 4], "little")
|
|
145
|
+
if value <= self.expected_maximum_points:
|
|
146
|
+
if self._column_count + len(columns) >= self.options.max_columns:
|
|
147
|
+
self.invalidate("Discovery column limit reached; no hypotheses were silently evicted.")
|
|
148
|
+
columns[offset] = _Column(value)
|
|
149
|
+
self._column_count += len(columns)
|
|
150
|
+
self._families[key] = _Family(caps, columns, observed_at)
|
|
151
|
+
self._candidates_dirty = True
|
|
152
|
+
|
|
153
|
+
def _update_family(self, key: _FamilyKey, message: bytes, observed_at: float) -> None:
|
|
154
|
+
family = self._families[key]
|
|
155
|
+
family.observed_at = observed_at
|
|
156
|
+
family.caps.intersection_update(
|
|
157
|
+
offset for offset in tuple(family.caps)
|
|
158
|
+
if int.from_bytes(message[offset : offset + 4], "little") == self.expected_maximum_points
|
|
159
|
+
)
|
|
160
|
+
for offset, column in tuple(family.columns.items()):
|
|
161
|
+
value = int.from_bytes(message[offset : offset + 4], "little")
|
|
162
|
+
selected_column = self._selected is not None and self._selected[:2] == (key, offset)
|
|
163
|
+
if not family.caps or value > self.expected_maximum_points or (value > column.last and not selected_column):
|
|
164
|
+
del family.columns[offset]
|
|
165
|
+
self._column_count -= 1
|
|
166
|
+
else:
|
|
167
|
+
# Only comparison counts support inference: never derive spending.
|
|
168
|
+
if value < column.last and column.distinct < self.options.minimum_updates:
|
|
169
|
+
column.distinct += 1
|
|
170
|
+
column.last = value
|
|
171
|
+
self._candidates_dirty = True
|
|
172
|
+
|
|
173
|
+
def _decode_selected(self, frame: BDOFrame) -> None:
|
|
174
|
+
assert self._selected is not None
|
|
175
|
+
_, offset, cap_offset = self._selected
|
|
176
|
+
remaining = int.from_bytes(frame.message[offset : offset + 4], "little")
|
|
177
|
+
maximum = int.from_bytes(frame.message[cap_offset : cap_offset + 4], "little")
|
|
178
|
+
if maximum != self.expected_maximum_points or remaining > maximum:
|
|
179
|
+
self.invalidate("Selected balance or cap is outside the validated layout constraints.")
|
|
180
|
+
self._latest_balance = AgrisBalance(
|
|
181
|
+
remaining_points=remaining,
|
|
182
|
+
maximum_points=maximum,
|
|
183
|
+
observed_at=frame.context.timestamp,
|
|
184
|
+
flow=_public_flow(frame.context.flow),
|
|
185
|
+
connection_epoch=frame.context.flow_generation,
|
|
186
|
+
)
|
|
187
|
+
self._selected_version += 1
|
|
188
|
+
|
|
189
|
+
def _find_candidates(self) -> None:
|
|
190
|
+
if not self._candidates_dirty:
|
|
191
|
+
return
|
|
192
|
+
candidates: list[_CandidateKey] = []
|
|
193
|
+
for key, family in self._families.items():
|
|
194
|
+
for offset, column in family.columns.items():
|
|
195
|
+
if column.distinct < self.options.minimum_updates:
|
|
196
|
+
continue
|
|
197
|
+
for cap_offset in sorted(family.caps):
|
|
198
|
+
if abs(cap_offset - offset) < 4:
|
|
199
|
+
continue
|
|
200
|
+
if len(candidates) >= self.options.max_candidates:
|
|
201
|
+
self.invalidate("Discovery candidate limit reached; uniqueness is unresolved.")
|
|
202
|
+
candidates.append((key, offset, cap_offset))
|
|
203
|
+
self._candidates = tuple(candidates)
|
|
204
|
+
self._candidates_dirty = False
|
|
205
|
+
|
|
206
|
+
def refresh(self, now: float | None = None) -> AgrisStatus:
|
|
207
|
+
"""Advance observation time and publish only the newest authorized balance.
|
|
208
|
+
|
|
209
|
+
Live owners must not advance time while accepted packets remain queued.
|
|
210
|
+
Offline owners use capture timestamps, never today's wall clock.
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
if now is not None:
|
|
214
|
+
if isinstance(now, bool) or not isinstance(now, (int, float)):
|
|
215
|
+
raise TypeError("now must be a finite number or None")
|
|
216
|
+
if not math.isfinite(now):
|
|
217
|
+
raise ValueError("now must be finite")
|
|
218
|
+
self._clock = max(self._clock, now)
|
|
219
|
+
if self._invalid_reason is not None:
|
|
220
|
+
return self.snapshot()
|
|
221
|
+
self._find_candidates()
|
|
222
|
+
unique = self._candidates[0] if len(self._candidates) == 1 else None
|
|
223
|
+
if unique != self._unique:
|
|
224
|
+
self._unique = unique
|
|
225
|
+
self._unique_since = self._clock
|
|
226
|
+
was_tracking = self._tracking
|
|
227
|
+
self._tracking = False
|
|
228
|
+
if unique is not None and self._clock - self._unique_since >= self.options.settle_seconds:
|
|
229
|
+
if self._selected is not None and unique != self._selected:
|
|
230
|
+
self.invalidate("A different hypothesis replaced the selected layout; restart discovery.")
|
|
231
|
+
if self._selected is None:
|
|
232
|
+
self._selected = unique
|
|
233
|
+
key, offset, _ = unique
|
|
234
|
+
family = self._families[key]
|
|
235
|
+
self._latest_balance = AgrisBalance(
|
|
236
|
+
remaining_points=family.columns[offset].last,
|
|
237
|
+
maximum_points=self.expected_maximum_points,
|
|
238
|
+
observed_at=family.observed_at,
|
|
239
|
+
flow=_public_flow(key[0]),
|
|
240
|
+
connection_epoch=key[1],
|
|
241
|
+
)
|
|
242
|
+
self._tracking = True
|
|
243
|
+
if self._tracking and (not was_tracking or self._published_version != self._selected_version):
|
|
244
|
+
assert self._latest_balance is not None
|
|
245
|
+
self._published_version = self._selected_version
|
|
246
|
+
if self._on_balance is not None:
|
|
247
|
+
self._on_balance(self._latest_balance)
|
|
248
|
+
return self.snapshot()
|
|
249
|
+
|
|
250
|
+
def snapshot(self) -> AgrisStatus:
|
|
251
|
+
"""Read immutable state without advancing time or emitting events."""
|
|
252
|
+
|
|
253
|
+
state: AgrisDiscoveryState
|
|
254
|
+
if self._invalid_reason is not None:
|
|
255
|
+
state = "invalid"
|
|
256
|
+
elif len(self._candidates) > 1:
|
|
257
|
+
state = "ambiguous"
|
|
258
|
+
elif not self._candidates:
|
|
259
|
+
state = "searching"
|
|
260
|
+
elif self._tracking:
|
|
261
|
+
state = "tracking"
|
|
262
|
+
else:
|
|
263
|
+
state = "settling"
|
|
264
|
+
return AgrisStatus(
|
|
265
|
+
status=state,
|
|
266
|
+
expected_maximum_points=self.expected_maximum_points,
|
|
267
|
+
balance=self._latest_balance if state == "tracking" else None,
|
|
268
|
+
candidates=tuple(
|
|
269
|
+
AgrisLayout(key[2], key[3], offset, cap_offset, _public_flow(key[0]), key[1])
|
|
270
|
+
for key, offset, cap_offset in self._candidates
|
|
271
|
+
),
|
|
272
|
+
observed_frames=self._observed_frames,
|
|
273
|
+
reason=self._invalid_reason,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
def close_flow(self, flow: FlowKey) -> None:
|
|
277
|
+
if self._selected is not None and self._selected[0][0] == flow:
|
|
278
|
+
self.invalidate("Selected connection ended; restart discovery for a new connection.")
|
|
279
|
+
for key in tuple(self._families):
|
|
280
|
+
if key[0] == flow:
|
|
281
|
+
self._column_count -= len(self._families.pop(key).columns)
|
|
282
|
+
self._candidates_dirty = True
|
|
283
|
+
self.refresh()
|
|
284
|
+
|
|
285
|
+
def gap_reset(self, flow: FlowKey, generation: int, resume_sequence: int) -> None:
|
|
286
|
+
# Reassembly gap recovery need not increment the connection generation.
|
|
287
|
+
self.invalidate("TCP gap detected; restart discovery with an intact capture.")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Validation shared by every Agris acquisition entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def validate_expected_maximum_points(value: object) -> int:
|
|
7
|
+
"""Require an explicit, positive uint32 cap; never infer a default."""
|
|
8
|
+
|
|
9
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
10
|
+
raise TypeError("expected_maximum_points must be an integer")
|
|
11
|
+
if not 1 <= value <= 0xFFFFFFFF:
|
|
12
|
+
raise ValueError("expected_maximum_points must be between 1 and 4294967295")
|
|
13
|
+
return value
|