skill-sentry-cli 2.0.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 (96) hide show
  1. skill_sentry_cli/__init__.py +5 -0
  2. skill_sentry_cli/__main__.py +4 -0
  3. skill_sentry_cli/cli.py +918 -0
  4. skill_sentry_cli/core/__init__.py +1 -0
  5. skill_sentry_cli/core/anchors/__init__.py +5 -0
  6. skill_sentry_cli/core/anchors/credential_flow.py +440 -0
  7. skill_sentry_cli/core/anchors/declared_hosts.py +78 -0
  8. skill_sentry_cli/core/cache/__init__.py +35 -0
  9. skill_sentry_cli/core/cache/hash_utils.py +206 -0
  10. skill_sentry_cli/core/cache/sqlite_store.py +925 -0
  11. skill_sentry_cli/core/config.py +299 -0
  12. skill_sentry_cli/core/contracts.py +551 -0
  13. skill_sentry_cli/core/decisions/__init__.py +44 -0
  14. skill_sentry_cli/core/decisions/base.py +174 -0
  15. skill_sentry_cli/core/decisions/calibration.py +325 -0
  16. skill_sentry_cli/core/decisions/laya.py +439 -0
  17. skill_sentry_cli/core/decisions/mapping.py +218 -0
  18. skill_sentry_cli/core/decisions/mock.py +118 -0
  19. skill_sentry_cli/core/decisions/questions.py +118 -0
  20. skill_sentry_cli/core/decisions/report.py +65 -0
  21. skill_sentry_cli/core/decisions/selector.py +151 -0
  22. skill_sentry_cli/core/engine.py +1412 -0
  23. skill_sentry_cli/core/evasion/__init__.py +27 -0
  24. skill_sentry_cli/core/evasion/detector.py +210 -0
  25. skill_sentry_cli/core/exit_codes.py +13 -0
  26. skill_sentry_cli/core/models/__init__.py +20 -0
  27. skill_sentry_cli/core/models/_json.py +147 -0
  28. skill_sentry_cli/core/models/base.py +50 -0
  29. skill_sentry_cli/core/models/gemma_cloud.py +240 -0
  30. skill_sentry_cli/core/models/gemma_local.py +273 -0
  31. skill_sentry_cli/core/models/image_utils.py +112 -0
  32. skill_sentry_cli/core/models/mock.py +152 -0
  33. skill_sentry_cli/core/models/selector.py +123 -0
  34. skill_sentry_cli/core/parser/__init__.py +29 -0
  35. skill_sentry_cli/core/parser/skill_parser.py +520 -0
  36. skill_sentry_cli/core/parser/typosquat.py +167 -0
  37. skill_sentry_cli/core/poisoned_well/__init__.py +52 -0
  38. skill_sentry_cli/core/poisoned_well/attacker_profile.py +230 -0
  39. skill_sentry_cli/core/poisoned_well/canary_gen.py +413 -0
  40. skill_sentry_cli/core/poisoned_well/exfil_tracker.py +470 -0
  41. skill_sentry_cli/core/reports.py +114 -0
  42. skill_sentry_cli/core/sandbox/__init__.py +74 -0
  43. skill_sentry_cli/core/sandbox/assets/Dockerfile.sandbox +39 -0
  44. skill_sentry_cli/core/sandbox/assets/masquerade.c +484 -0
  45. skill_sentry_cli/core/sandbox/assets/run_skill.sh +41 -0
  46. skill_sentry_cli/core/sandbox/assets/sinkhole_sidecar.py +171 -0
  47. skill_sentry_cli/core/sandbox/canary_fs.py +108 -0
  48. skill_sentry_cli/core/sandbox/context_honeypot.py +97 -0
  49. skill_sentry_cli/core/sandbox/dns_sinkhole.py +218 -0
  50. skill_sentry_cli/core/sandbox/fake_http.py +194 -0
  51. skill_sentry_cli/core/sandbox/instructions.py +170 -0
  52. skill_sentry_cli/core/sandbox/masquerade.py +265 -0
  53. skill_sentry_cli/core/sandbox/monitor.py +278 -0
  54. skill_sentry_cli/core/sandbox/runner.py +776 -0
  55. skill_sentry_cli/core/sandbox/sidecar.py +327 -0
  56. skill_sentry_cli/core/semantic/__init__.py +9 -0
  57. skill_sentry_cli/core/semantic/analyzer.py +443 -0
  58. skill_sentry_cli/core/semantic/ast_anchor.py +371 -0
  59. skill_sentry_cli/core/semantic/ast_python.py +156 -0
  60. skill_sentry_cli/core/semantic/candidates.py +78 -0
  61. skill_sentry_cli/core/semantic/laya_analyzer.py +241 -0
  62. skill_sentry_cli/core/semantic/prompts.py +61 -0
  63. skill_sentry_cli/core/verdict/__init__.py +24 -0
  64. skill_sentry_cli/core/verdict/fusion.py +392 -0
  65. skill_sentry_cli/core/verdict/markdown_report.py +211 -0
  66. skill_sentry_cli/core/verdict/sarif_report.py +139 -0
  67. skill_sentry_cli/core/verdict/status.py +35 -0
  68. skill_sentry_cli/core/verdict/stix_report.py +132 -0
  69. skill_sentry_cli/core/version.py +38 -0
  70. skill_sentry_cli/core/vision/__init__.py +8 -0
  71. skill_sentry_cli/core/vision/image_extractor.py +91 -0
  72. skill_sentry_cli/core/vision/vision_scanner.py +274 -0
  73. skill_sentry_cli/data/__init__.py +1 -0
  74. skill_sentry_cli/data/known_skills_v1.json +276 -0
  75. skill_sentry_cli/data/sandbox_image.json +4 -0
  76. skill_sentry_cli/rules/credential_rules.toml +94 -0
  77. skill_sentry_cli/server/__init__.py +5 -0
  78. skill_sentry_cli/server/app.py +129 -0
  79. skill_sentry_cli/server/backend.py +63 -0
  80. skill_sentry_cli/server/demo_assets.py +62 -0
  81. skill_sentry_cli/server/demo_backend.py +475 -0
  82. skill_sentry_cli/server/demo_data.py +814 -0
  83. skill_sentry_cli/server/demo_mode.py +40 -0
  84. skill_sentry_cli/server/errors.py +48 -0
  85. skill_sentry_cli/server/live_backend.py +576 -0
  86. skill_sentry_cli/server/routes.py +191 -0
  87. skill_sentry_cli/skill/SKILL.md +67 -0
  88. skill_sentry_cli/skill/scripts/audit.sh +48 -0
  89. skill_sentry_cli/ui/dist/assets/index-Bf0h24IX.css +1 -0
  90. skill_sentry_cli/ui/dist/assets/index-CrYBKSSB.js +8 -0
  91. skill_sentry_cli/ui/dist/index.html +14 -0
  92. skill_sentry_cli-2.0.0.dist-info/METADATA +76 -0
  93. skill_sentry_cli-2.0.0.dist-info/RECORD +96 -0
  94. skill_sentry_cli-2.0.0.dist-info/WHEEL +4 -0
  95. skill_sentry_cli-2.0.0.dist-info/entry_points.txt +2 -0
  96. skill_sentry_cli-2.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,118 @@
1
+ """Deterministic decision client for unit tests and local wiring checks.
2
+
3
+ ``MockDecisionClient`` is never selected by the production decision selector.
4
+ It exists so tests can exercise the semantic stage without downloading a Laya
5
+ checkpoint. A mock-only scan must never be presented as live-model evidence.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ from skill_sentry_cli.core.decisions.base import (
13
+ DecisionAnswer,
14
+ DecisionQuestion,
15
+ DecisionResult,
16
+ )
17
+
18
+ _MALICIOUS_MARKERS = (
19
+ "aws/credentials",
20
+ "base64",
21
+ "curl ",
22
+ "dockerenv",
23
+ "evil",
24
+ "exfil",
25
+ "nslookup",
26
+ "private key",
27
+ "token",
28
+ )
29
+
30
+
31
+ class MockDecisionClient:
32
+ """Deterministic typed decisions with ``auto``/``benign``/``malicious`` profiles."""
33
+
34
+ backend = "mock"
35
+ model_version = "mock-laya-v1"
36
+
37
+ def __init__(
38
+ self,
39
+ profile: str = "auto",
40
+ *,
41
+ answers: dict[str, DecisionAnswer] | None = None,
42
+ ) -> None:
43
+ if profile not in {"auto", "benign", "malicious"}:
44
+ raise ValueError(f"unknown MockDecisionClient profile: {profile}")
45
+ self.profile = profile
46
+ self._answers = dict(answers or {})
47
+
48
+ def decide(
49
+ self,
50
+ state: str | dict[str, Any],
51
+ questions: dict[str, DecisionQuestion],
52
+ *,
53
+ timeout_s: float | None = None,
54
+ ) -> DecisionResult:
55
+ del timeout_s
56
+ malicious = self._is_malicious(state)
57
+ answers: dict[str, DecisionAnswer] = {}
58
+ for qid, question in questions.items():
59
+ if qid in self._answers:
60
+ answers[qid] = self._answers[qid]
61
+ else:
62
+ answers[qid] = _mock_answer(qid, question, malicious)
63
+ return DecisionResult(
64
+ answers=answers,
65
+ backend=self.backend,
66
+ checkpoint="mock",
67
+ model_version=self.model_version,
68
+ routing={"model": "mock", "reason": "deterministic test double"},
69
+ usage={"input_tokens": 0, "output_tokens": 0},
70
+ )
71
+
72
+ def health(self) -> bool:
73
+ return True
74
+
75
+ def close(self) -> None:
76
+ return None
77
+
78
+ def _is_malicious(self, state: str | dict[str, Any]) -> bool:
79
+ if self.profile == "malicious":
80
+ return True
81
+ if self.profile == "benign":
82
+ return False
83
+ text = state if isinstance(state, str) else str(state)
84
+ lowered = text.lower()
85
+ return any(marker in lowered for marker in _MALICIOUS_MARKERS)
86
+
87
+
88
+ def _mock_answer(qid: str, question: DecisionQuestion, malicious: bool) -> DecisionAnswer:
89
+ if question.type == "choice":
90
+ criteria = question.criteria if isinstance(question.criteria, dict) else {}
91
+ keys = list(criteria)
92
+ if qid == "deception" and "deceptive" in criteria:
93
+ choice = "deceptive" if malicious else "consistent"
94
+ elif malicious and "data_export" in criteria:
95
+ choice = "data_export"
96
+ elif not malicious and "other" in criteria:
97
+ choice = "other"
98
+ else:
99
+ choice = keys[0] if keys else "other"
100
+ if len(keys) == 1:
101
+ probabilities: dict[str, float] = {keys[0]: 1.0}
102
+ else:
103
+ rest = (1.0 - 0.9) / max(1, len(keys) - 1)
104
+ probabilities = {key: (0.9 if key == choice else rest) for key in keys}
105
+ return DecisionAnswer(choice=choice, probabilities=probabilities, confidence=0.9)
106
+
107
+ if question.type == "score":
108
+ criteria = question.criteria if isinstance(question.criteria, list) else []
109
+ selected = 1.0 if malicious else 0.0
110
+ index = 1 if malicious and len(criteria) > 1 else 0
111
+ rest = (1.0 - 0.9) / max(1, len(criteria) - 1)
112
+ probabilities = [0.9 if position == index else rest for position in range(len(criteria))]
113
+ return DecisionAnswer(score=selected, probabilities=probabilities, confidence=0.9)
114
+
115
+ return DecisionAnswer(noul=0.9 if malicious else 0.05, confidence=0.9)
116
+
117
+
118
+ __all__ = ["MockDecisionClient"]
@@ -0,0 +1,118 @@
1
+ """Fixed semantic question taxonomy for local decision backends.
2
+
3
+ The taxonomy is deliberately small. Large choice sets degrade Laya accuracy
4
+ and make calibration harder. Add a capability only when there is a
5
+ deterministic evidence source or a clear evaluation case for it.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Iterable
11
+
12
+ from skill_sentry_cli.core.decisions.base import DecisionQuestion, EvidenceCandidate
13
+
14
+ INTENT_CHOICES: dict[str, str] = {
15
+ "text_processing": "Summarising, translating, transforming, or generating text.",
16
+ "file_management": "Reading, writing, moving, or organising files and directories.",
17
+ "network_access": "Calling APIs, downloading resources, or otherwise using the network.",
18
+ "credential_access": "Reading, requesting, or transmitting credentials or secrets.",
19
+ "code_execution": "Running commands, scripts, or arbitrary code.",
20
+ "ui_automation": "Interacting with a browser, desktop UI, or other graphical surface.",
21
+ "data_export": "Uploading, exporting, or transferring user or system data.",
22
+ "other": "None of the categories above is a good fit.",
23
+ }
24
+
25
+ DECEPTION_CHOICES: dict[str, str] = {
26
+ "consistent": "The implementation is consistent with the declared purpose.",
27
+ "unclear": "The available evidence is insufficient to decide.",
28
+ "deceptive": "The implementation hides or contradicts its declared purpose.",
29
+ }
30
+
31
+ # Question IDs are stable: they appear in telemetry, calibration files, and
32
+ # report metadata. Do not rename one without a migration.
33
+ CAPABILITY_QUESTIONS: dict[str, str] = {
34
+ "cap_credentials": (
35
+ "Does the skill read, request, or transmit credential material such as "
36
+ "passwords, API keys, tokens, private keys, or cloud credentials?"
37
+ ),
38
+ "cap_env_secrets": (
39
+ "Does the skill inspect environment variables or shell profiles to discover "
40
+ "secrets or credentials?"
41
+ ),
42
+ "cap_process": "Does the skill spawn processes or execute shell commands?",
43
+ "cap_network": "Does the skill make outbound network requests?",
44
+ "cap_upload": "Does the skill send data to a remote endpoint?",
45
+ "cap_dns": "Does the skill perform DNS lookups or encode data in DNS queries?",
46
+ "cap_filesystem": (
47
+ "Does the skill write, modify, or delete files outside its own package directory?"
48
+ ),
49
+ "cap_persistence": (
50
+ "Does the skill modify startup, shell profile, scheduled task, or other "
51
+ "persistence mechanisms?"
52
+ ),
53
+ "cap_obfuscation": (
54
+ "Does the skill decode, encrypt, or otherwise obfuscate content to hide its behavior?"
55
+ ),
56
+ "cap_hidden_behavior": "Does the skill hide behavior from the user or operator?",
57
+ "cap_prompt_injection": (
58
+ "Does the skill contain instructions aimed at an auditing agent or language "
59
+ "model rather than at its own runtime?"
60
+ ),
61
+ "cap_deception": "Does the implementation contradict its declared intent?",
62
+ }
63
+
64
+
65
+ def adjudication_questions(candidates: Iterable[EvidenceCandidate]) -> dict[str, DecisionQuestion]:
66
+ """One yes/no question per candidate, keyed ``adj_<candidate_id>``.
67
+
68
+ The question names only the candidate's code-owned anchor type. The code
69
+ itself travels in the state, never in the instructions.
70
+ """
71
+
72
+ return {
73
+ f"adj_{candidate.candidate_id}": DecisionQuestion(
74
+ type="noul",
75
+ instructions=(
76
+ f"Is the code candidate {candidate.candidate_id} (anchor type "
77
+ f"{candidate.anchor_type}) used for a harmful purpose rather than the "
78
+ "skill's declared function?"
79
+ ),
80
+ )
81
+ for candidate in candidates
82
+ }
83
+
84
+
85
+ def semantic_questions() -> dict[str, DecisionQuestion]:
86
+ """Return a fresh copy of the V1 semantic question set.
87
+
88
+ A fresh mapping is returned so callers cannot mutate the module-level
89
+ taxonomy while building a request.
90
+ """
91
+
92
+ questions: dict[str, DecisionQuestion] = {
93
+ "intent": DecisionQuestion(
94
+ type="choice",
95
+ instructions="Which category best describes the skill's declared purpose?",
96
+ criteria=dict(INTENT_CHOICES),
97
+ ),
98
+ "deception": DecisionQuestion(
99
+ type="choice",
100
+ instructions=(
101
+ "Does the implementation behave consistently with the declared purpose, "
102
+ "or does it hide or contradict that purpose?"
103
+ ),
104
+ criteria=dict(DECEPTION_CHOICES),
105
+ ),
106
+ }
107
+ for question_id, instructions in CAPABILITY_QUESTIONS.items():
108
+ questions[question_id] = DecisionQuestion(type="noul", instructions=instructions)
109
+ return questions
110
+
111
+
112
+ __all__ = [
113
+ "CAPABILITY_QUESTIONS",
114
+ "DECEPTION_CHOICES",
115
+ "INTENT_CHOICES",
116
+ "adjudication_questions",
117
+ "semantic_questions",
118
+ ]
@@ -0,0 +1,65 @@
1
+ """Report metadata for the typed decision layer.
2
+
3
+ The summary is built from a whitelist of stage-telemetry keys. It never copies
4
+ prompts, routing payloads, or skill text, so a report cannot leak them even if
5
+ a backend adds fields to its response.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Iterable
11
+ from typing import Any
12
+
13
+ from skill_sentry_cli.core.contracts import StageResult
14
+
15
+ _SEMANTIC_KEYS = (
16
+ "decision_backend",
17
+ "model_backend",
18
+ "checkpoint",
19
+ "model_version",
20
+ "calibration_version",
21
+ "instruction_floor",
22
+ )
23
+ _VISION_STATUSES = frozenset({"not_applicable", "evaluated", "partial", "not_evaluated"})
24
+
25
+
26
+ def decision_summary(stage_results: Iterable[StageResult]) -> dict[str, Any]:
27
+ """Return the report-safe decision summary for one scan's stage results."""
28
+
29
+ summary: dict[str, Any] = {
30
+ "decision_backend": "legacy",
31
+ "checkpoint": None,
32
+ "model_version": None,
33
+ "calibration_version": "uncalibrated",
34
+ "semantic_degraded": False,
35
+ "semantic_error": None,
36
+ "threshold": None,
37
+ "capabilities": {},
38
+ "vision_status": "not_evaluated",
39
+ }
40
+ for result in stage_results:
41
+ if result.stage.value == "semantic":
42
+ telemetry = result.telemetry or {}
43
+ for key in _SEMANTIC_KEYS:
44
+ if telemetry.get(key) is not None:
45
+ summary[key] = telemetry[key]
46
+ decisions = telemetry.get("decisions") or {}
47
+ if isinstance(decisions, dict):
48
+ summary["threshold"] = decisions.get("threshold")
49
+ capabilities = decisions.get("capabilities")
50
+ if isinstance(capabilities, dict):
51
+ summary["capabilities"] = {
52
+ str(key): round(float(value), 4)
53
+ for key, value in capabilities.items()
54
+ if isinstance(value, (int, float))
55
+ }
56
+ summary["semantic_degraded"] = bool(result.degraded or result.error)
57
+ summary["semantic_error"] = str(result.error)[:300] if result.error else None
58
+ elif result.stage.value == "vision":
59
+ status = (result.telemetry or {}).get("vision_status")
60
+ if status in _VISION_STATUSES:
61
+ summary["vision_status"] = status
62
+ return summary
63
+
64
+
65
+ __all__ = ["decision_summary"]
@@ -0,0 +1,151 @@
1
+ """Decision-backend selection.
2
+
3
+ This is separate from the legacy generation/vision selector. ``auto`` prefers
4
+ Laya when the package and router configuration are available, falls back to the
5
+ legacy semantic path only when the caller says it exists, and otherwise returns
6
+ an explicit degraded selection. A missing required decision backend never
7
+ silently becomes a clean scan.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+
14
+ from skill_sentry_cli.core.config import DecisionSettings, SkillsEntryConfig
15
+ from skill_sentry_cli.core.decisions.base import DecisionClient, DecisionUnavailable
16
+ from skill_sentry_cli.core.decisions.laya import LayaDecisionClient
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class DecisionSelection:
21
+ """Result of decision-backend selection.
22
+
23
+ ``client`` is set only for a typed decision backend. ``backend`` may be
24
+ ``laya``, ``legacy``, or ``none`` so callers can route the legacy semantic
25
+ analyzer separately and record the degraded reason.
26
+ """
27
+
28
+ client: DecisionClient | None
29
+ backend: str
30
+ degraded: bool = False
31
+ reason: str | None = None
32
+ required: bool = False
33
+
34
+ @property
35
+ def available(self) -> bool:
36
+ return self.client is not None
37
+
38
+
39
+ def select_decision_model(
40
+ config: SkillsEntryConfig | DecisionSettings | None = None,
41
+ *,
42
+ backend: str | None = None,
43
+ probe: bool = True,
44
+ laya_client: DecisionClient | None = None,
45
+ legacy_available: bool = False,
46
+ ) -> DecisionSelection:
47
+ """Return the typed decision backend or an explicit fallback/degradation.
48
+
49
+ ``auto`` prefers Laya if it imports and its router can be constructed. It
50
+ does not load checkpoint weights; the first :meth:`decide` call does that
51
+ and any load failure becomes a degraded stage. Explicit ``laya`` fails
52
+ closed with :class:`DecisionUnavailable` when the health probe fails.
53
+ """
54
+
55
+ settings = _decision_settings(config)
56
+ choice = _backend_value(backend if backend is not None else settings.backend)
57
+ required = settings.required
58
+
59
+ if choice == "none":
60
+ return DecisionSelection(
61
+ client=None,
62
+ backend="none",
63
+ degraded=True,
64
+ reason="decision backend disabled; semantic analysis is deterministic-only",
65
+ required=required,
66
+ )
67
+ if choice == "legacy":
68
+ return DecisionSelection(
69
+ client=None,
70
+ backend="legacy",
71
+ degraded=not legacy_available,
72
+ reason=None if legacy_available else "legacy semantic model is not available",
73
+ required=required,
74
+ )
75
+ if choice not in {"auto", "laya"}:
76
+ raise ValueError(f"unsupported decision backend: {choice!r}")
77
+
78
+ client = laya_client or LayaDecisionClient(settings)
79
+ healthy = True if not probe else _healthy(client)
80
+ if healthy:
81
+ return DecisionSelection(
82
+ client=client,
83
+ backend="laya",
84
+ degraded=False,
85
+ required=required,
86
+ )
87
+
88
+ error = _failure_detail(client)
89
+ if choice == "laya":
90
+ raise DecisionUnavailable(f"laya decision backend is not available: {error}")
91
+
92
+ if required:
93
+ return DecisionSelection(
94
+ client=None,
95
+ backend="none",
96
+ degraded=True,
97
+ reason=f"laya is required but unavailable: {error}",
98
+ required=True,
99
+ )
100
+ if legacy_available:
101
+ return DecisionSelection(
102
+ client=None,
103
+ backend="legacy",
104
+ degraded=False,
105
+ reason=f"laya unavailable; using legacy semantic path: {error}",
106
+ required=required,
107
+ )
108
+ return DecisionSelection(
109
+ client=None,
110
+ backend="none",
111
+ degraded=True,
112
+ reason=f"no decision backend available: {error}",
113
+ required=required,
114
+ )
115
+
116
+
117
+ def _decision_settings(
118
+ config: SkillsEntryConfig | DecisionSettings | None,
119
+ ) -> DecisionSettings:
120
+ if config is None:
121
+ return DecisionSettings()
122
+ if isinstance(config, DecisionSettings):
123
+ return config
124
+ return config.decision
125
+
126
+
127
+ def _backend_value(value: str) -> str:
128
+ normalised = str(value).strip().lower()
129
+ return {
130
+ "laya-local": "laya",
131
+ "local": "laya",
132
+ "off": "none",
133
+ "disabled": "none",
134
+ }.get(normalised, normalised)
135
+
136
+
137
+ def _healthy(client: DecisionClient) -> bool:
138
+ try:
139
+ return bool(client.health())
140
+ except Exception:
141
+ return False
142
+
143
+
144
+ def _failure_detail(client: DecisionClient) -> str:
145
+ detail = getattr(client, "last_error", None)
146
+ if isinstance(detail, str) and detail.strip():
147
+ return detail
148
+ return "health probe failed"
149
+
150
+
151
+ __all__ = ["DecisionSelection", "select_decision_model"]