mimvo 0.2.0__py3-none-any.whl

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.
Files changed (90) hide show
  1. mimvo/__init__.py +53 -0
  2. mimvo/_coerce.py +72 -0
  3. mimvo/_version.py +11 -0
  4. mimvo/checks/__init__.py +54 -0
  5. mimvo/checks/base.py +149 -0
  6. mimvo/checks/security/__init__.py +43 -0
  7. mimvo/checks/security/_hf.py +439 -0
  8. mimvo/checks/security/_http.py +62 -0
  9. mimvo/checks/security/_phrases.py +180 -0
  10. mimvo/checks/security/base.py +780 -0
  11. mimvo/checks/security/injection/__init__.py +48 -0
  12. mimvo/checks/security/injection/check.py +45 -0
  13. mimvo/checks/security/injection/datasentinel.py +81 -0
  14. mimvo/checks/security/injection/deepset_deberta.py +31 -0
  15. mimvo/checks/security/injection/embedding_clf.py +76 -0
  16. mimvo/checks/security/injection/heuristic.py +429 -0
  17. mimvo/checks/security/injection/known_answer.py +76 -0
  18. mimvo/checks/security/injection/lakera_guard.py +129 -0
  19. mimvo/checks/security/injection/probes.py +80 -0
  20. mimvo/checks/security/injection/prompt_guard.py +37 -0
  21. mimvo/checks/security/injection/prompt_shield.py +127 -0
  22. mimvo/checks/security/injection/protectai_deberta.py +31 -0
  23. mimvo/checks/security/injection/sentinel.py +44 -0
  24. mimvo/checks/security/poisoning/__init__.py +50 -0
  25. mimvo/checks/security/poisoning/_corpus.py +28 -0
  26. mimvo/checks/security/poisoning/check.py +48 -0
  27. mimvo/checks/security/poisoning/consistency.py +101 -0
  28. mimvo/checks/security/poisoning/heuristic.py +453 -0
  29. mimvo/checks/security/poisoning/hubness.py +109 -0
  30. mimvo/checks/security/poisoning/nli.py +127 -0
  31. mimvo/checks/security/poisoning/perplexity.py +140 -0
  32. mimvo/checks/security/poisoning/probe.py +115 -0
  33. mimvo/checks/security/poisoning/raguard.py +149 -0
  34. mimvo/checks/security/poisoning/revprag.py +51 -0
  35. mimvo/checks/security/poisoning/trustrag.py +192 -0
  36. mimvo/checks/security/secrets/__init__.py +86 -0
  37. mimvo/checks/security/secrets/check.py +51 -0
  38. mimvo/checks/security/secrets/detect_secrets.py +118 -0
  39. mimvo/checks/security/secrets/entropy.py +156 -0
  40. mimvo/checks/security/secrets/gitleaks.py +79 -0
  41. mimvo/checks/security/secrets/gliner2_pii.py +184 -0
  42. mimvo/checks/security/secrets/gliner_pii.py +150 -0
  43. mimvo/checks/security/secrets/heuristic.py +151 -0
  44. mimvo/checks/security/secrets/piiranha.py +77 -0
  45. mimvo/checks/security/secrets/presidio.py +161 -0
  46. mimvo/checks/security/secrets/starpii.py +54 -0
  47. mimvo/checks/security/secrets/verify.py +62 -0
  48. mimvo/cli.py +556 -0
  49. mimvo/client.py +205 -0
  50. mimvo/config.py +45 -0
  51. mimvo/context.py +107 -0
  52. mimvo/corpus.py +708 -0
  53. mimvo/engine.py +350 -0
  54. mimvo/exceptions.py +56 -0
  55. mimvo/integrations/__init__.py +16 -0
  56. mimvo/integrations/hook.py +216 -0
  57. mimvo/mask.py +87 -0
  58. mimvo/models/__init__.py +36 -0
  59. mimvo/models/enums.py +171 -0
  60. mimvo/models/finding.py +76 -0
  61. mimvo/models/memory.py +79 -0
  62. mimvo/models/results.py +366 -0
  63. mimvo/owasp.py +39 -0
  64. mimvo/plugins.py +142 -0
  65. mimvo/py.typed +0 -0
  66. mimvo/reporters/__init__.py +17 -0
  67. mimvo/reporters/html.py +900 -0
  68. mimvo/reporters/markdown.py +267 -0
  69. mimvo/reporters/sarif.py +222 -0
  70. mimvo/reporters/terminal.py +177 -0
  71. mimvo/rules.py +486 -0
  72. mimvo/sources/__init__.py +33 -0
  73. mimvo/sources/base.py +261 -0
  74. mimvo/sources/chroma.py +141 -0
  75. mimvo/sources/jsonl.py +85 -0
  76. mimvo/sources/langchain.py +251 -0
  77. mimvo/sources/langgraph.py +137 -0
  78. mimvo/sources/mem0.py +238 -0
  79. mimvo/sources/pgvector.py +160 -0
  80. mimvo/sources/pinecone.py +154 -0
  81. mimvo/sources/qdrant.py +140 -0
  82. mimvo/telemetry/__init__.py +116 -0
  83. mimvo/telemetry/otel.py +79 -0
  84. mimvo/text.py +469 -0
  85. mimvo/vectors.py +155 -0
  86. mimvo-0.2.0.dist-info/METADATA +684 -0
  87. mimvo-0.2.0.dist-info/RECORD +90 -0
  88. mimvo-0.2.0.dist-info/WHEEL +4 -0
  89. mimvo-0.2.0.dist-info/entry_points.txt +2 -0
  90. mimvo-0.2.0.dist-info/licenses/LICENSE +201 -0
mimvo/__init__.py ADDED
@@ -0,0 +1,53 @@
1
+ """Scan agent memory for poisoned facts, hidden instructions, and leaked secrets.
2
+
3
+ From the command line:
4
+
5
+ mimvo scan chroma --path ./chroma_db --collection agent_memory
6
+ mimvo scan jsonl export.jsonl --report report.html --json findings.json
7
+
8
+ From Python, create a `Mimvo` and call `scan`. The scan reads records and
9
+ returns a `ScanReport`. It does not change the store.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from ._version import __version__
15
+ from .client import AsyncMimvo, Mimvo
16
+ from .config import Config
17
+ from .exceptions import (
18
+ BackendError,
19
+ ConfigurationError,
20
+ MimvoError,
21
+ )
22
+ from .models import (
23
+ Action,
24
+ Category,
25
+ Finding,
26
+ MemoryCandidate,
27
+ MemoryRecord,
28
+ MemoryStatus,
29
+ Risk,
30
+ ScanError,
31
+ ScanReport,
32
+ Severity,
33
+ )
34
+
35
+ __all__ = [
36
+ "Action",
37
+ "AsyncMimvo",
38
+ "BackendError",
39
+ "Category",
40
+ "Config",
41
+ "ConfigurationError",
42
+ "Finding",
43
+ "MemoryCandidate",
44
+ "MemoryRecord",
45
+ "MemoryStatus",
46
+ "Mimvo",
47
+ "MimvoError",
48
+ "Risk",
49
+ "ScanError",
50
+ "ScanReport",
51
+ "Severity",
52
+ "__version__",
53
+ ]
mimvo/_coerce.py ADDED
@@ -0,0 +1,72 @@
1
+ """Turn loose caller input into `MemoryCandidate` and `MemoryRecord`.
2
+
3
+ Callers can pass a string, a dict, or an existing model. These helpers
4
+ accept all three so the common case stays short.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from .exceptions import ConfigurationError
10
+ from .models.memory import MemoryCandidate, MemoryRecord
11
+
12
+
13
+ def coerce_candidate(content: str | MemoryCandidate | dict) -> MemoryCandidate:
14
+ """Turn one input into a `MemoryCandidate`.
15
+
16
+ Args:
17
+ content: The memory to scan. A string becomes the `content` field.
18
+ A dict is validated as a `MemoryCandidate`. A `MemoryCandidate`
19
+ is returned unchanged.
20
+
21
+ Returns:
22
+ A `MemoryCandidate` the checks can read.
23
+
24
+ Raises:
25
+ ConfigurationError: `content` is not a string, a dict, or a
26
+ `MemoryCandidate`.
27
+ """
28
+ if isinstance(content, MemoryCandidate):
29
+ return content
30
+ if isinstance(content, dict):
31
+ return MemoryCandidate.model_validate(content)
32
+ if isinstance(content, str):
33
+ return MemoryCandidate(content=content)
34
+ raise ConfigurationError(f"Cannot interpret content: {content!r}")
35
+
36
+
37
+ def coerce_record(item: object) -> MemoryRecord:
38
+ """Turn one input into a `MemoryRecord`.
39
+
40
+ Args:
41
+ item: A `MemoryRecord`, returned unchanged, or a dict with at least
42
+ an `id` and a `content` string.
43
+
44
+ Returns:
45
+ A `MemoryRecord` the scan engine can read.
46
+
47
+ Raises:
48
+ ConfigurationError: `item` is not a record or a dict.
49
+ """
50
+ if isinstance(item, MemoryRecord):
51
+ return item
52
+ if isinstance(item, dict):
53
+ return MemoryRecord.model_validate(item)
54
+ raise ConfigurationError(f"Cannot interpret record: {item!r}")
55
+
56
+
57
+ def coerce_records(records: list) -> list[MemoryRecord]:
58
+ """Turn a list of records or dicts into `MemoryRecord` objects.
59
+
60
+ Args:
61
+ records: Each item is passed to `coerce_record`.
62
+
63
+ Returns:
64
+ One `MemoryRecord` per item, in the same order.
65
+
66
+ Raises:
67
+ ConfigurationError: Any item is not a record or a dict.
68
+ """
69
+ return [coerce_record(item) for item in records]
70
+
71
+
72
+ __all__ = ["coerce_candidate", "coerce_record", "coerce_records"]
mimvo/_version.py ADDED
@@ -0,0 +1,11 @@
1
+ """The installed Mimvo version, in a module with no imports.
2
+
3
+ Reports stamp this onto their output. Keeping it here lets the models
4
+ import it without importing the whole package.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ __version__ = "0.2.0"
10
+
11
+ __all__ = ["__version__"]
@@ -0,0 +1,54 @@
1
+ """Built-in checks and `default_checks`.
2
+
3
+ `default_checks()` returns the standard pipeline: secrets, injection, then
4
+ poisoning. Each one uses an offline detector unless you pass your own.
5
+
6
+ Pass a configured `InjectionCheck`, `PoisoningCheck`, or `SecretsCheck` in
7
+ `Mimvo(checks=[...])` to replace the default check of the same name.
8
+ A check with a new name is added alongside the defaults.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from .base import MemoryCheck, Phase
14
+ from .security import (
15
+ BaseDetector,
16
+ Detection,
17
+ Detector,
18
+ InjectionCheck,
19
+ PoisoningCheck,
20
+ SecretsCheck,
21
+ SecurityCheck,
22
+ )
23
+
24
+
25
+ def default_checks() -> list[MemoryCheck]:
26
+ """Return the checks `Mimvo()` runs, in order.
27
+
28
+ The order is secrets, then injection, then poisoning. Each check uses
29
+ its offline detector, so this list needs no network and no model download.
30
+ The same input always produces the same findings.
31
+
32
+ Returns:
33
+ A new list of check instances. Call it again for a fresh list; do
34
+ not share one list across clients.
35
+ """
36
+ return [
37
+ SecretsCheck(),
38
+ InjectionCheck(),
39
+ PoisoningCheck(),
40
+ ]
41
+
42
+
43
+ __all__ = [
44
+ "BaseDetector",
45
+ "Detection",
46
+ "Detector",
47
+ "InjectionCheck",
48
+ "MemoryCheck",
49
+ "Phase",
50
+ "PoisoningCheck",
51
+ "SecretsCheck",
52
+ "SecurityCheck",
53
+ "default_checks",
54
+ ]
mimvo/checks/base.py ADDED
@@ -0,0 +1,149 @@
1
+ """The `MemoryCheck` base class and the contract the engine runs it through.
2
+
3
+ A check looks at one memory and returns findings. The simplest check sets
4
+ `name` and implements `check`. Adding a check does not require editing the
5
+ engine: the engine only uses the methods defined here.
6
+
7
+ A scan reads the store once, as a stream, then runs a second pass over the
8
+ packed corpus for work that compares records with each other. A check says
9
+ which passes it needs (`phases`), records what it saw on each pass
10
+ (`observe`), and turns those observations into findings once its last pass
11
+ is done (`conclude`). The defaults wrap `check`, so a plain check never
12
+ has to think about passes; `SecurityCheck` overrides them to split its
13
+ detectors across the passes and vote over both.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from collections.abc import Sequence
19
+ from enum import StrEnum
20
+
21
+ from ..context import CheckContext
22
+ from ..models.finding import Finding
23
+ from ..models.memory import MemoryCandidate
24
+
25
+
26
+ class Phase(StrEnum):
27
+ """The two passes of a scan.
28
+
29
+ Attributes:
30
+ STREAM: While records are read, one at a time. Only the record
31
+ itself (and records already read) can be seen.
32
+ CORPUS: After every record has been read. `context.existing` and
33
+ the corpus hold the whole scan.
34
+ """
35
+
36
+ STREAM = "stream"
37
+ CORPUS = "corpus"
38
+
39
+
40
+ class MemoryCheck:
41
+ """One named check over a single memory.
42
+
43
+ Set `name` and implement `check`. The built-in security checks subclass
44
+ `SecurityCheck`, which subclasses this.
45
+
46
+ Attributes:
47
+ name: Label stored on each finding this check emits. The client uses
48
+ it to replace a default check: a custom check named `"injection"`
49
+ takes the place of the built-in injection check.
50
+ needs_corpus: Whether `check` reads other records. `True` (or
51
+ unset) runs it once every record has been read, so
52
+ `context.existing` is the whole scan; `False` runs it while
53
+ records stream in.
54
+ """
55
+
56
+ name: str = "base"
57
+ needs_corpus: bool | None = None
58
+
59
+ def check(self, candidate: MemoryCandidate, context: CheckContext) -> list[Finding]:
60
+ """Inspect one memory and return every problem found.
61
+
62
+ Args:
63
+ candidate: The memory being scanned. Read `candidate.content`.
64
+ context: Scan settings, the clock, neighbouring records, and the
65
+ optional retrieval query.
66
+
67
+ Returns:
68
+ Findings for this memory. An empty list means this check found
69
+ nothing. Never return `None`.
70
+
71
+ Raises:
72
+ NotImplementedError: Always, unless a subclass implements this.
73
+ """
74
+ raise NotImplementedError
75
+
76
+ # -- engine contract ---------------------------------------------------------
77
+
78
+ def phases(self) -> frozenset[Phase]:
79
+ """Which passes this check runs in.
80
+
81
+ Returns:
82
+ `{CORPUS}` unless `needs_corpus` is `False`, then `{STREAM}`.
83
+ A check that returns both is observed on each pass and
84
+ concluded after the corpus pass.
85
+ """
86
+ return frozenset({Phase.STREAM if self.needs_corpus is False else Phase.CORPUS})
87
+
88
+ def observe(self, candidate: MemoryCandidate, context: CheckContext, phase: Phase) -> object:
89
+ """Look at one memory during one pass.
90
+
91
+ The engine keeps whatever this returns and hands every observation
92
+ for the record to `conclude` after the check's last pass.
93
+
94
+ Args:
95
+ candidate: The memory being scanned.
96
+ context: Scan context.
97
+ phase: The pass being run. Always one of `phases()`.
98
+
99
+ Returns:
100
+ An observation only this check needs to understand. The default
101
+ is the list of findings `check` returned. An empty (falsy)
102
+ observation means "nothing seen": the engine may drop it rather
103
+ than keep it until the corpus pass.
104
+ """
105
+ return self.check(candidate, context)
106
+
107
+ def observe_batch(
108
+ self,
109
+ candidates: Sequence[MemoryCandidate],
110
+ context: CheckContext,
111
+ phase: Phase,
112
+ ) -> list[object]:
113
+ """Look at several memories during one pass.
114
+
115
+ The engine hands each pass to checks in batches of
116
+ `Config.batch_size` records, so a check whose work batches well,
117
+ such as a model forward pass, can do it once per batch.
118
+
119
+ Args:
120
+ candidates: The memories, in scan order.
121
+ context: Scan context.
122
+ phase: The pass being run.
123
+
124
+ Returns:
125
+ One observation per candidate, in the same order. The default
126
+ calls `observe` on each.
127
+ """
128
+ return [self.observe(candidate, context, phase) for candidate in candidates]
129
+
130
+ def conclude(self, observations: Sequence[object]) -> list[Finding]:
131
+ """Turn one record's observations into findings.
132
+
133
+ Args:
134
+ observations: What `observe` returned for this record, one per
135
+ pass the check ran in, in pass order.
136
+
137
+ Returns:
138
+ The record's findings. The default joins the finding lists.
139
+ The engine stamps this check's `name` on any finding whose
140
+ `check` is unset.
141
+ """
142
+ findings: list[Finding] = []
143
+ for observation in observations:
144
+ if isinstance(observation, list):
145
+ findings.extend(f for f in observation if isinstance(f, Finding))
146
+ return findings
147
+
148
+
149
+ __all__ = ["MemoryCheck", "Phase"]
@@ -0,0 +1,43 @@
1
+ """Security checks: injection, poisoning, and secrets.
2
+
3
+ Layout:
4
+
5
+ security/
6
+ base.py SecurityCheck, Detector, Detection
7
+ injection/ InjectionCheck and one detector per method
8
+ poisoning/ PoisoningCheck and one detector per method
9
+ secrets/ SecretsCheck and one detector per method
10
+
11
+ Each check runs detectors and turns their hits into findings. The defaults
12
+ are offline heuristics. Model and hosted detectors are optional.
13
+
14
+ Example:
15
+ Replace the default injection check with heuristic plus Prompt Guard::
16
+
17
+ from mimvo import Mimvo
18
+ from mimvo.checks.security import InjectionCheck
19
+ from mimvo.checks.security.injection import (
20
+ HeuristicInjectionDetector, PromptGuardDetector,
21
+ )
22
+
23
+ guard = Mimvo(checks=[
24
+ InjectionCheck(detectors=[HeuristicInjectionDetector(), PromptGuardDetector()]),
25
+ ])
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ from .base import BaseDetector, Detection, Detector, SecurityCheck
31
+ from .injection import InjectionCheck
32
+ from .poisoning import PoisoningCheck
33
+ from .secrets import SecretsCheck
34
+
35
+ __all__ = [
36
+ "BaseDetector",
37
+ "Detection",
38
+ "Detector",
39
+ "InjectionCheck",
40
+ "PoisoningCheck",
41
+ "SecretsCheck",
42
+ "SecurityCheck",
43
+ ]