agent_os_kernel 3.1.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.
- agent_control_plane/__init__.py +662 -0
- agent_control_plane/a2a_adapter.py +543 -0
- agent_control_plane/adapter.py +417 -0
- agent_control_plane/agent_hibernation.py +394 -0
- agent_control_plane/agent_kernel.py +470 -0
- agent_control_plane/compliance.py +720 -0
- agent_control_plane/constraint_graphs.py +478 -0
- agent_control_plane/control_plane.py +854 -0
- agent_control_plane/example_executors.py +195 -0
- agent_control_plane/execution_engine.py +231 -0
- agent_control_plane/flight_recorder.py +846 -0
- agent_control_plane/governance_layer.py +435 -0
- agent_control_plane/hf_utils.py +563 -0
- agent_control_plane/interfaces/__init__.py +55 -0
- agent_control_plane/interfaces/kernel_interface.py +361 -0
- agent_control_plane/interfaces/plugin_interface.py +497 -0
- agent_control_plane/interfaces/protocol_interfaces.py +387 -0
- agent_control_plane/kernel_space.py +1009 -0
- agent_control_plane/langchain_adapter.py +424 -0
- agent_control_plane/lifecycle.py +3113 -0
- agent_control_plane/mcp_adapter.py +653 -0
- agent_control_plane/ml_safety.py +563 -0
- agent_control_plane/multimodal.py +727 -0
- agent_control_plane/mute_agent.py +422 -0
- agent_control_plane/observability.py +787 -0
- agent_control_plane/orchestrator.py +482 -0
- agent_control_plane/plugin_registry.py +750 -0
- agent_control_plane/policy_engine.py +954 -0
- agent_control_plane/process_isolation.py +777 -0
- agent_control_plane/shadow_mode.py +310 -0
- agent_control_plane/signals.py +493 -0
- agent_control_plane/supervisor_agents.py +430 -0
- agent_control_plane/time_travel_debugger.py +557 -0
- agent_control_plane/tool_registry.py +452 -0
- agent_control_plane/vfs.py +697 -0
- agent_kernel/__init__.py +69 -0
- agent_kernel/analyzer.py +435 -0
- agent_kernel/auditor.py +36 -0
- agent_kernel/completeness_auditor.py +237 -0
- agent_kernel/detector.py +203 -0
- agent_kernel/kernel.py +744 -0
- agent_kernel/memory_manager.py +85 -0
- agent_kernel/models.py +374 -0
- agent_kernel/nudge_mechanism.py +263 -0
- agent_kernel/outcome_analyzer.py +338 -0
- agent_kernel/patcher.py +582 -0
- agent_kernel/semantic_analyzer.py +316 -0
- agent_kernel/semantic_purge.py +349 -0
- agent_kernel/simulator.py +449 -0
- agent_kernel/teacher.py +85 -0
- agent_kernel/triage.py +152 -0
- agent_os/__init__.py +409 -0
- agent_os/_adversarial_impl.py +200 -0
- agent_os/_circuit_breaker_impl.py +232 -0
- agent_os/_mcp_metrics.py +193 -0
- agent_os/adversarial.py +20 -0
- agent_os/agents_compat.py +490 -0
- agent_os/audit_logger.py +135 -0
- agent_os/base_agent.py +651 -0
- agent_os/circuit_breaker.py +34 -0
- agent_os/cli/__init__.py +659 -0
- agent_os/cli/cmd_audit.py +128 -0
- agent_os/cli/cmd_init.py +152 -0
- agent_os/cli/cmd_policy.py +41 -0
- agent_os/cli/cmd_policy_gen.py +180 -0
- agent_os/cli/cmd_validate.py +258 -0
- agent_os/cli/mcp_scan.py +265 -0
- agent_os/cli/output.py +192 -0
- agent_os/cli/policy_checker.py +330 -0
- agent_os/compat.py +74 -0
- agent_os/constraint_graph.py +234 -0
- agent_os/content_governance.py +140 -0
- agent_os/context_budget.py +305 -0
- agent_os/credential_redactor.py +224 -0
- agent_os/diff_policy.py +89 -0
- agent_os/egress_policy.py +159 -0
- agent_os/escalation.py +276 -0
- agent_os/event_bus.py +124 -0
- agent_os/exceptions.py +180 -0
- agent_os/execution_context_policy.py +141 -0
- agent_os/github_enterprise.py +96 -0
- agent_os/health.py +20 -0
- agent_os/integrations/__init__.py +279 -0
- agent_os/integrations/a2a_adapter.py +279 -0
- agent_os/integrations/agent_lightning/__init__.py +30 -0
- agent_os/integrations/anthropic_adapter.py +420 -0
- agent_os/integrations/autogen_adapter.py +620 -0
- agent_os/integrations/base.py +1137 -0
- agent_os/integrations/compat.py +229 -0
- agent_os/integrations/config.py +98 -0
- agent_os/integrations/conversation_guardian.py +957 -0
- agent_os/integrations/crewai_adapter.py +467 -0
- agent_os/integrations/drift_detector.py +425 -0
- agent_os/integrations/dry_run.py +124 -0
- agent_os/integrations/escalation.py +582 -0
- agent_os/integrations/gemini_adapter.py +364 -0
- agent_os/integrations/google_adk_adapter.py +633 -0
- agent_os/integrations/guardrails_adapter.py +394 -0
- agent_os/integrations/health.py +197 -0
- agent_os/integrations/langchain_adapter.py +654 -0
- agent_os/integrations/llamafirewall.py +343 -0
- agent_os/integrations/llamaindex_adapter.py +188 -0
- agent_os/integrations/logging.py +191 -0
- agent_os/integrations/maf_adapter.py +631 -0
- agent_os/integrations/mistral_adapter.py +365 -0
- agent_os/integrations/openai_adapter.py +816 -0
- agent_os/integrations/openai_agents_sdk.py +406 -0
- agent_os/integrations/policy_compose.py +171 -0
- agent_os/integrations/profiling.py +144 -0
- agent_os/integrations/pydantic_ai_adapter.py +420 -0
- agent_os/integrations/rate_limiter.py +130 -0
- agent_os/integrations/rbac.py +143 -0
- agent_os/integrations/registry.py +113 -0
- agent_os/integrations/scope_guard.py +303 -0
- agent_os/integrations/semantic_kernel_adapter.py +769 -0
- agent_os/integrations/smolagents_adapter.py +629 -0
- agent_os/integrations/templates.py +178 -0
- agent_os/integrations/token_budget.py +134 -0
- agent_os/integrations/tool_aliases.py +190 -0
- agent_os/integrations/webhooks.py +177 -0
- agent_os/lite.py +208 -0
- agent_os/mcp_gateway.py +385 -0
- agent_os/mcp_message_signer.py +273 -0
- agent_os/mcp_protocols.py +161 -0
- agent_os/mcp_response_scanner.py +232 -0
- agent_os/mcp_security.py +924 -0
- agent_os/mcp_session_auth.py +231 -0
- agent_os/mcp_sliding_rate_limiter.py +184 -0
- agent_os/memory_guard.py +409 -0
- agent_os/metrics.py +134 -0
- agent_os/mute.py +428 -0
- agent_os/mute_agent.py +209 -0
- agent_os/policies/__init__.py +77 -0
- agent_os/policies/async_evaluator.py +275 -0
- agent_os/policies/backends.py +670 -0
- agent_os/policies/bridge.py +169 -0
- agent_os/policies/budget.py +85 -0
- agent_os/policies/cli.py +294 -0
- agent_os/policies/conflict_resolution.py +270 -0
- agent_os/policies/data_classification.py +252 -0
- agent_os/policies/evaluator.py +239 -0
- agent_os/policies/policy_schema.json +228 -0
- agent_os/policies/rate_limiting.py +145 -0
- agent_os/policies/schema.py +115 -0
- agent_os/policies/shared.py +331 -0
- agent_os/prompt_injection.py +694 -0
- agent_os/providers.py +182 -0
- agent_os/py.typed +0 -0
- agent_os/retry.py +81 -0
- agent_os/reversibility.py +251 -0
- agent_os/sandbox.py +432 -0
- agent_os/sandbox_provider.py +140 -0
- agent_os/secure_codegen.py +525 -0
- agent_os/security_skills.py +538 -0
- agent_os/semantic_policy.py +422 -0
- agent_os/server/__init__.py +15 -0
- agent_os/server/__main__.py +25 -0
- agent_os/server/app.py +277 -0
- agent_os/server/models.py +104 -0
- agent_os/shift_left_metrics.py +130 -0
- agent_os/stateless.py +742 -0
- agent_os/supervisor.py +148 -0
- agent_os/task_outcome.py +148 -0
- agent_os/transparency.py +181 -0
- agent_os/trust_root.py +128 -0
- agent_os_kernel-3.1.0.dist-info/METADATA +1269 -0
- agent_os_kernel-3.1.0.dist-info/RECORD +337 -0
- agent_os_kernel-3.1.0.dist-info/WHEEL +4 -0
- agent_os_kernel-3.1.0.dist-info/entry_points.txt +2 -0
- agent_os_kernel-3.1.0.dist-info/licenses/LICENSE +21 -0
- agent_os_observability/__init__.py +27 -0
- agent_os_observability/dashboards.py +898 -0
- agent_os_observability/metrics.py +398 -0
- agent_os_observability/server.py +223 -0
- agent_os_observability/tracer.py +232 -0
- agent_primitives/__init__.py +24 -0
- agent_primitives/failures.py +84 -0
- agent_primitives/py.typed +0 -0
- amb_core/__init__.py +177 -0
- amb_core/adapters/__init__.py +57 -0
- amb_core/adapters/aws_sqs_broker.py +376 -0
- amb_core/adapters/azure_servicebus_broker.py +340 -0
- amb_core/adapters/kafka_broker.py +260 -0
- amb_core/adapters/nats_broker.py +285 -0
- amb_core/adapters/rabbitmq_broker.py +235 -0
- amb_core/adapters/redis_broker.py +262 -0
- amb_core/broker.py +145 -0
- amb_core/bus.py +481 -0
- amb_core/cloudevents.py +509 -0
- amb_core/dlq.py +345 -0
- amb_core/hf_utils.py +536 -0
- amb_core/memory_broker.py +410 -0
- amb_core/models.py +141 -0
- amb_core/persistence.py +529 -0
- amb_core/schema.py +294 -0
- amb_core/tracing.py +358 -0
- atr/__init__.py +640 -0
- atr/access.py +348 -0
- atr/composition.py +645 -0
- atr/decorator.py +357 -0
- atr/executor.py +384 -0
- atr/health.py +557 -0
- atr/hf_utils.py +449 -0
- atr/injection.py +422 -0
- atr/metrics.py +440 -0
- atr/policies.py +403 -0
- atr/py.typed +2 -0
- atr/registry.py +452 -0
- atr/schema.py +480 -0
- atr/tools/safe/__init__.py +75 -0
- atr/tools/safe/calculator.py +467 -0
- atr/tools/safe/datetime_tool.py +443 -0
- atr/tools/safe/file_reader.py +402 -0
- atr/tools/safe/http_client.py +316 -0
- atr/tools/safe/json_parser.py +374 -0
- atr/tools/safe/text_tool.py +537 -0
- atr/tools/safe/toolkit.py +175 -0
- caas/__init__.py +162 -0
- caas/api/__init__.py +7 -0
- caas/api/server.py +1328 -0
- caas/caching.py +834 -0
- caas/cli.py +210 -0
- caas/conversation.py +223 -0
- caas/decay.py +72 -0
- caas/detection/__init__.py +9 -0
- caas/detection/detector.py +238 -0
- caas/enrichment.py +130 -0
- caas/gateway/__init__.py +27 -0
- caas/gateway/trust_gateway.py +474 -0
- caas/hf_utils.py +479 -0
- caas/ingestion/__init__.py +23 -0
- caas/ingestion/processors.py +253 -0
- caas/ingestion/structure_parser.py +188 -0
- caas/models.py +356 -0
- caas/pragmatic_truth.py +444 -0
- caas/routing/__init__.py +10 -0
- caas/routing/heuristic_router.py +58 -0
- caas/storage/__init__.py +9 -0
- caas/storage/store.py +389 -0
- caas/triad.py +213 -0
- caas/tuning/__init__.py +9 -0
- caas/tuning/tuner.py +329 -0
- caas/vfs/__init__.py +14 -0
- caas/vfs/filesystem.py +452 -0
- cmvk/__init__.py +218 -0
- cmvk/audit.py +402 -0
- cmvk/benchmarks.py +478 -0
- cmvk/constitutional.py +904 -0
- cmvk/hf_utils.py +301 -0
- cmvk/metrics.py +473 -0
- cmvk/profiles.py +300 -0
- cmvk/py.typed +0 -0
- cmvk/types.py +12 -0
- cmvk/verification.py +956 -0
- emk/__init__.py +89 -0
- emk/causal.py +352 -0
- emk/hf_utils.py +421 -0
- emk/indexer.py +83 -0
- emk/py.typed +0 -0
- emk/schema.py +204 -0
- emk/sleep_cycle.py +347 -0
- emk/store.py +281 -0
- iatp/__init__.py +166 -0
- iatp/attestation.py +461 -0
- iatp/cli.py +317 -0
- iatp/hf_utils.py +472 -0
- iatp/ipc_pipes.py +580 -0
- iatp/main.py +412 -0
- iatp/models/__init__.py +447 -0
- iatp/policy_engine.py +337 -0
- iatp/py.typed +2 -0
- iatp/recovery.py +321 -0
- iatp/security/__init__.py +270 -0
- iatp/sidecar/__init__.py +519 -0
- iatp/telemetry/__init__.py +164 -0
- iatp/tests/__init__.py +1 -0
- iatp/tests/test_attestation.py +370 -0
- iatp/tests/test_cli.py +131 -0
- iatp/tests/test_ed25519_attestation.py +211 -0
- iatp/tests/test_models.py +130 -0
- iatp/tests/test_policy_engine.py +347 -0
- iatp/tests/test_recovery.py +281 -0
- iatp/tests/test_security.py +222 -0
- iatp/tests/test_sidecar.py +167 -0
- iatp/tests/test_telemetry.py +175 -0
- mcp_kernel_server/__init__.py +28 -0
- mcp_kernel_server/cli.py +274 -0
- mcp_kernel_server/resources.py +217 -0
- mcp_kernel_server/server.py +564 -0
- mcp_kernel_server/tools.py +1174 -0
- mute_agent/__init__.py +68 -0
- mute_agent/core/__init__.py +1 -0
- mute_agent/core/execution_agent.py +166 -0
- mute_agent/core/handshake_protocol.py +201 -0
- mute_agent/core/reasoning_agent.py +238 -0
- mute_agent/knowledge_graph/__init__.py +1 -0
- mute_agent/knowledge_graph/graph_elements.py +65 -0
- mute_agent/knowledge_graph/multidimensional_graph.py +170 -0
- mute_agent/knowledge_graph/subgraph.py +224 -0
- mute_agent/listener/__init__.py +43 -0
- mute_agent/listener/adapters/__init__.py +31 -0
- mute_agent/listener/adapters/base_adapter.py +189 -0
- mute_agent/listener/adapters/caas_adapter.py +344 -0
- mute_agent/listener/adapters/control_plane_adapter.py +436 -0
- mute_agent/listener/adapters/iatp_adapter.py +332 -0
- mute_agent/listener/adapters/scak_adapter.py +251 -0
- mute_agent/listener/listener.py +610 -0
- mute_agent/listener/state_observer.py +436 -0
- mute_agent/listener/threshold_config.py +313 -0
- mute_agent/super_system/__init__.py +1 -0
- mute_agent/super_system/router.py +204 -0
- mute_agent/visualization/__init__.py +10 -0
- mute_agent/visualization/graph_debugger.py +502 -0
- nexus/README.md +60 -0
- nexus/__init__.py +51 -0
- nexus/arbiter.py +359 -0
- nexus/client.py +466 -0
- nexus/dmz.py +444 -0
- nexus/escrow.py +430 -0
- nexus/exceptions.py +286 -0
- nexus/pyproject.toml +36 -0
- nexus/registry.py +393 -0
- nexus/reputation.py +425 -0
- nexus/schemas/__init__.py +51 -0
- nexus/schemas/compliance.py +276 -0
- nexus/schemas/escrow.py +251 -0
- nexus/schemas/manifest.py +225 -0
- nexus/schemas/receipt.py +208 -0
- nexus/tests/__init__.py +0 -0
- nexus/tests/conftest.py +146 -0
- nexus/tests/test_arbiter.py +192 -0
- nexus/tests/test_dmz.py +194 -0
- nexus/tests/test_escrow.py +276 -0
- nexus/tests/test_exceptions.py +225 -0
- nexus/tests/test_registry.py +232 -0
- nexus/tests/test_reputation.py +328 -0
- nexus/tests/test_schemas.py +295 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""
|
|
4
|
+
Safe Text Processing Tool.
|
|
5
|
+
|
|
6
|
+
Provides safe text operations with:
|
|
7
|
+
- No regex with catastrophic backtracking
|
|
8
|
+
- Size limits
|
|
9
|
+
- Safe string operations only
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import re
|
|
14
|
+
import hashlib
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
from atr.decorator import tool
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TextTool:
|
|
23
|
+
"""
|
|
24
|
+
Safe text processing operations.
|
|
25
|
+
|
|
26
|
+
Features:
|
|
27
|
+
- String manipulation (split, join, replace, etc.)
|
|
28
|
+
- Safe regex with timeout protection
|
|
29
|
+
- Text analysis (word count, etc.)
|
|
30
|
+
- Encoding/decoding
|
|
31
|
+
- No arbitrary code execution
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
```python
|
|
35
|
+
text = TextTool(max_length=100000)
|
|
36
|
+
|
|
37
|
+
# Basic operations
|
|
38
|
+
result = text.split("hello world", " ")
|
|
39
|
+
result = text.replace("hello", "hi", "hello world")
|
|
40
|
+
|
|
41
|
+
# Analysis
|
|
42
|
+
stats = text.analyze("Hello world!")
|
|
43
|
+
|
|
44
|
+
# Safe regex
|
|
45
|
+
matches = text.regex_find(r"\\d+", "abc123def456")
|
|
46
|
+
```
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
max_length: int = 1_000_000,
|
|
52
|
+
max_regex_length: int = 200,
|
|
53
|
+
max_matches: int = 1000
|
|
54
|
+
):
|
|
55
|
+
"""
|
|
56
|
+
Initialize text tool.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
max_length: Maximum text length to process
|
|
60
|
+
max_regex_length: Maximum regex pattern length
|
|
61
|
+
max_matches: Maximum regex matches to return
|
|
62
|
+
"""
|
|
63
|
+
self.max_length = max_length
|
|
64
|
+
self.max_regex_length = max_regex_length
|
|
65
|
+
self.max_matches = max_matches
|
|
66
|
+
|
|
67
|
+
def _check_length(self, text: str, name: str = "text"):
|
|
68
|
+
"""Check text length."""
|
|
69
|
+
if len(text) > self.max_length:
|
|
70
|
+
raise ValueError(f"{name} too long: {len(text)}. Max: {self.max_length}")
|
|
71
|
+
|
|
72
|
+
def _validate_regex(self, pattern: str):
|
|
73
|
+
"""Validate regex pattern for safety."""
|
|
74
|
+
if len(pattern) > self.max_regex_length:
|
|
75
|
+
raise ValueError(f"Regex pattern too long. Max: {self.max_regex_length}")
|
|
76
|
+
|
|
77
|
+
# Check for potentially catastrophic patterns
|
|
78
|
+
dangerous_patterns = [
|
|
79
|
+
r"(.+)+", # Nested quantifiers
|
|
80
|
+
r"(.*)*",
|
|
81
|
+
r"(a+)+",
|
|
82
|
+
r"(a*)*",
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
for dangerous in dangerous_patterns:
|
|
86
|
+
if dangerous in pattern:
|
|
87
|
+
raise ValueError(f"Potentially dangerous regex pattern detected")
|
|
88
|
+
|
|
89
|
+
# Try to compile
|
|
90
|
+
try:
|
|
91
|
+
re.compile(pattern)
|
|
92
|
+
except re.error as e:
|
|
93
|
+
raise ValueError(f"Invalid regex: {e}")
|
|
94
|
+
|
|
95
|
+
@tool(
|
|
96
|
+
name="text_split",
|
|
97
|
+
description="Split text by a delimiter",
|
|
98
|
+
tags=["text", "split", "safe"]
|
|
99
|
+
)
|
|
100
|
+
def split(
|
|
101
|
+
self,
|
|
102
|
+
text: str,
|
|
103
|
+
delimiter: str = " ",
|
|
104
|
+
max_splits: Optional[int] = None
|
|
105
|
+
) -> Dict[str, Any]:
|
|
106
|
+
"""
|
|
107
|
+
Split text by delimiter.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
text: Text to split
|
|
111
|
+
delimiter: Delimiter string
|
|
112
|
+
max_splits: Maximum number of splits
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Dict with parts list
|
|
116
|
+
"""
|
|
117
|
+
try:
|
|
118
|
+
self._check_length(text)
|
|
119
|
+
|
|
120
|
+
if max_splits:
|
|
121
|
+
parts = text.split(delimiter, max_splits)
|
|
122
|
+
else:
|
|
123
|
+
parts = text.split(delimiter)
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
"success": True,
|
|
127
|
+
"parts": parts,
|
|
128
|
+
"count": len(parts)
|
|
129
|
+
}
|
|
130
|
+
except Exception as e:
|
|
131
|
+
return {"success": False, "error": str(e)}
|
|
132
|
+
|
|
133
|
+
@tool(
|
|
134
|
+
name="text_join",
|
|
135
|
+
description="Join text parts with a delimiter",
|
|
136
|
+
tags=["text", "join", "safe"]
|
|
137
|
+
)
|
|
138
|
+
def join(self, parts: List[str], delimiter: str = " ") -> Dict[str, Any]:
|
|
139
|
+
"""
|
|
140
|
+
Join text parts.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
parts: List of strings to join
|
|
144
|
+
delimiter: Delimiter string
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Dict with joined text
|
|
148
|
+
"""
|
|
149
|
+
try:
|
|
150
|
+
result = delimiter.join(parts)
|
|
151
|
+
self._check_length(result, "result")
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
"success": True,
|
|
155
|
+
"result": result,
|
|
156
|
+
"length": len(result)
|
|
157
|
+
}
|
|
158
|
+
except Exception as e:
|
|
159
|
+
return {"success": False, "error": str(e)}
|
|
160
|
+
|
|
161
|
+
@tool(
|
|
162
|
+
name="text_replace",
|
|
163
|
+
description="Replace occurrences in text",
|
|
164
|
+
tags=["text", "replace", "safe"]
|
|
165
|
+
)
|
|
166
|
+
def replace(
|
|
167
|
+
self,
|
|
168
|
+
text: str,
|
|
169
|
+
old: str,
|
|
170
|
+
new: str,
|
|
171
|
+
count: int = -1
|
|
172
|
+
) -> Dict[str, Any]:
|
|
173
|
+
"""
|
|
174
|
+
Replace text occurrences.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
text: Input text
|
|
178
|
+
old: String to find
|
|
179
|
+
new: Replacement string
|
|
180
|
+
count: Max replacements (-1 for all)
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Dict with result
|
|
184
|
+
"""
|
|
185
|
+
try:
|
|
186
|
+
self._check_length(text)
|
|
187
|
+
|
|
188
|
+
if count == -1:
|
|
189
|
+
result = text.replace(old, new)
|
|
190
|
+
else:
|
|
191
|
+
result = text.replace(old, new, count)
|
|
192
|
+
|
|
193
|
+
replacements = text.count(old) if count == -1 else min(text.count(old), count)
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
"success": True,
|
|
197
|
+
"result": result,
|
|
198
|
+
"replacements": replacements
|
|
199
|
+
}
|
|
200
|
+
except Exception as e:
|
|
201
|
+
return {"success": False, "error": str(e)}
|
|
202
|
+
|
|
203
|
+
@tool(
|
|
204
|
+
name="text_analyze",
|
|
205
|
+
description="Analyze text and return statistics",
|
|
206
|
+
tags=["text", "analyze", "safe"]
|
|
207
|
+
)
|
|
208
|
+
def analyze(self, text: str) -> Dict[str, Any]:
|
|
209
|
+
"""
|
|
210
|
+
Analyze text statistics.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
text: Text to analyze
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Dict with text statistics
|
|
217
|
+
"""
|
|
218
|
+
try:
|
|
219
|
+
self._check_length(text)
|
|
220
|
+
|
|
221
|
+
words = text.split()
|
|
222
|
+
lines = text.splitlines()
|
|
223
|
+
sentences = re.split(r'[.!?]+', text)
|
|
224
|
+
sentences = [s.strip() for s in sentences if s.strip()]
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
"success": True,
|
|
228
|
+
"characters": len(text),
|
|
229
|
+
"characters_no_spaces": len(text.replace(" ", "")),
|
|
230
|
+
"words": len(words),
|
|
231
|
+
"lines": len(lines),
|
|
232
|
+
"sentences": len(sentences),
|
|
233
|
+
"paragraphs": len(text.split("\n\n")),
|
|
234
|
+
"avg_word_length": round(sum(len(w) for w in words) / max(len(words), 1), 2),
|
|
235
|
+
"unique_words": len(set(w.lower() for w in words))
|
|
236
|
+
}
|
|
237
|
+
except Exception as e:
|
|
238
|
+
return {"success": False, "error": str(e)}
|
|
239
|
+
|
|
240
|
+
@tool(
|
|
241
|
+
name="text_regex_find",
|
|
242
|
+
description="Find all regex matches in text",
|
|
243
|
+
tags=["text", "regex", "safe"]
|
|
244
|
+
)
|
|
245
|
+
def regex_find(
|
|
246
|
+
self,
|
|
247
|
+
pattern: str,
|
|
248
|
+
text: str,
|
|
249
|
+
flags: Optional[str] = None
|
|
250
|
+
) -> Dict[str, Any]:
|
|
251
|
+
"""
|
|
252
|
+
Find regex matches.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
pattern: Regex pattern
|
|
256
|
+
text: Text to search
|
|
257
|
+
flags: Optional flags (i=ignorecase, m=multiline, s=dotall)
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Dict with matches
|
|
261
|
+
"""
|
|
262
|
+
try:
|
|
263
|
+
self._check_length(text)
|
|
264
|
+
self._validate_regex(pattern)
|
|
265
|
+
|
|
266
|
+
# Parse flags
|
|
267
|
+
re_flags = 0
|
|
268
|
+
if flags:
|
|
269
|
+
if 'i' in flags.lower():
|
|
270
|
+
re_flags |= re.IGNORECASE
|
|
271
|
+
if 'm' in flags.lower():
|
|
272
|
+
re_flags |= re.MULTILINE
|
|
273
|
+
if 's' in flags.lower():
|
|
274
|
+
re_flags |= re.DOTALL
|
|
275
|
+
|
|
276
|
+
matches = re.findall(pattern, text, re_flags)
|
|
277
|
+
|
|
278
|
+
# Limit matches
|
|
279
|
+
if len(matches) > self.max_matches:
|
|
280
|
+
matches = matches[:self.max_matches]
|
|
281
|
+
truncated = True
|
|
282
|
+
else:
|
|
283
|
+
truncated = False
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
"success": True,
|
|
287
|
+
"matches": matches,
|
|
288
|
+
"count": len(matches),
|
|
289
|
+
"truncated": truncated
|
|
290
|
+
}
|
|
291
|
+
except Exception as e:
|
|
292
|
+
return {"success": False, "error": str(e)}
|
|
293
|
+
|
|
294
|
+
@tool(
|
|
295
|
+
name="text_regex_replace",
|
|
296
|
+
description="Replace regex matches in text",
|
|
297
|
+
tags=["text", "regex", "safe"]
|
|
298
|
+
)
|
|
299
|
+
def regex_replace(
|
|
300
|
+
self,
|
|
301
|
+
pattern: str,
|
|
302
|
+
replacement: str,
|
|
303
|
+
text: str,
|
|
304
|
+
count: int = 0
|
|
305
|
+
) -> Dict[str, Any]:
|
|
306
|
+
"""
|
|
307
|
+
Replace regex matches.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
pattern: Regex pattern
|
|
311
|
+
replacement: Replacement string
|
|
312
|
+
text: Text to process
|
|
313
|
+
count: Max replacements (0 for all)
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
Dict with result
|
|
317
|
+
"""
|
|
318
|
+
try:
|
|
319
|
+
self._check_length(text)
|
|
320
|
+
self._validate_regex(pattern)
|
|
321
|
+
|
|
322
|
+
result, num_subs = re.subn(pattern, replacement, text, count=count)
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
"success": True,
|
|
326
|
+
"result": result,
|
|
327
|
+
"replacements": num_subs
|
|
328
|
+
}
|
|
329
|
+
except Exception as e:
|
|
330
|
+
return {"success": False, "error": str(e)}
|
|
331
|
+
|
|
332
|
+
@tool(
|
|
333
|
+
name="text_trim",
|
|
334
|
+
description="Trim whitespace from text",
|
|
335
|
+
tags=["text", "trim", "safe"]
|
|
336
|
+
)
|
|
337
|
+
def trim(
|
|
338
|
+
self,
|
|
339
|
+
text: str,
|
|
340
|
+
chars: Optional[str] = None,
|
|
341
|
+
side: str = "both"
|
|
342
|
+
) -> Dict[str, Any]:
|
|
343
|
+
"""
|
|
344
|
+
Trim characters from text.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
text: Text to trim
|
|
348
|
+
chars: Characters to trim (default: whitespace)
|
|
349
|
+
side: "left", "right", or "both"
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
Dict with trimmed text
|
|
353
|
+
"""
|
|
354
|
+
try:
|
|
355
|
+
if side == "left":
|
|
356
|
+
result = text.lstrip(chars)
|
|
357
|
+
elif side == "right":
|
|
358
|
+
result = text.rstrip(chars)
|
|
359
|
+
else:
|
|
360
|
+
result = text.strip(chars)
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
"success": True,
|
|
364
|
+
"result": result,
|
|
365
|
+
"removed": len(text) - len(result)
|
|
366
|
+
}
|
|
367
|
+
except Exception as e:
|
|
368
|
+
return {"success": False, "error": str(e)}
|
|
369
|
+
|
|
370
|
+
@tool(
|
|
371
|
+
name="text_case",
|
|
372
|
+
description="Change text case",
|
|
373
|
+
tags=["text", "case", "safe"]
|
|
374
|
+
)
|
|
375
|
+
def change_case(self, text: str, case: str = "lower") -> Dict[str, Any]:
|
|
376
|
+
"""
|
|
377
|
+
Change text case.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
text: Text to transform
|
|
381
|
+
case: "lower", "upper", "title", "capitalize", "swapcase"
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
Dict with transformed text
|
|
385
|
+
"""
|
|
386
|
+
try:
|
|
387
|
+
self._check_length(text)
|
|
388
|
+
|
|
389
|
+
case_funcs = {
|
|
390
|
+
"lower": str.lower,
|
|
391
|
+
"upper": str.upper,
|
|
392
|
+
"title": str.title,
|
|
393
|
+
"capitalize": str.capitalize,
|
|
394
|
+
"swapcase": str.swapcase
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if case not in case_funcs:
|
|
398
|
+
return {
|
|
399
|
+
"success": False,
|
|
400
|
+
"error": f"Unknown case: {case}. Use: {', '.join(case_funcs.keys())}"
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
result = case_funcs[case](text)
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
"success": True,
|
|
407
|
+
"result": result,
|
|
408
|
+
"case": case
|
|
409
|
+
}
|
|
410
|
+
except Exception as e:
|
|
411
|
+
return {"success": False, "error": str(e)}
|
|
412
|
+
|
|
413
|
+
@tool(
|
|
414
|
+
name="text_hash",
|
|
415
|
+
description="Generate hash of text",
|
|
416
|
+
tags=["text", "hash", "safe"]
|
|
417
|
+
)
|
|
418
|
+
def hash(self, text: str, algorithm: str = "sha256") -> Dict[str, Any]:
|
|
419
|
+
"""
|
|
420
|
+
Generate hash of text.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
text: Text to hash
|
|
424
|
+
algorithm: Hash algorithm (md5, sha1, sha256, sha512)
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
Dict with hash
|
|
428
|
+
"""
|
|
429
|
+
try:
|
|
430
|
+
algorithms = {
|
|
431
|
+
"md5": hashlib.md5,
|
|
432
|
+
"sha1": hashlib.sha1,
|
|
433
|
+
"sha256": hashlib.sha256,
|
|
434
|
+
"sha512": hashlib.sha512
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if algorithm in ("md5", "sha1"):
|
|
438
|
+
logger.warning(
|
|
439
|
+
"Algorithm '%s' is deprecated due to CWE-328. Use 'sha256' or 'sha512'.",
|
|
440
|
+
algorithm,
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
if algorithm not in algorithms:
|
|
444
|
+
return {
|
|
445
|
+
"success": False,
|
|
446
|
+
"error": f"Unknown algorithm. Use: {', '.join(algorithms.keys())}"
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
hasher = algorithms[algorithm]()
|
|
450
|
+
hasher.update(text.encode('utf-8'))
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
"success": True,
|
|
454
|
+
"hash": hasher.hexdigest(),
|
|
455
|
+
"algorithm": algorithm
|
|
456
|
+
}
|
|
457
|
+
except Exception as e:
|
|
458
|
+
return {"success": False, "error": str(e)}
|
|
459
|
+
|
|
460
|
+
@tool(
|
|
461
|
+
name="text_contains",
|
|
462
|
+
description="Check if text contains a substring",
|
|
463
|
+
tags=["text", "search", "safe"]
|
|
464
|
+
)
|
|
465
|
+
def contains(
|
|
466
|
+
self,
|
|
467
|
+
text: str,
|
|
468
|
+
substring: str,
|
|
469
|
+
case_sensitive: bool = True
|
|
470
|
+
) -> Dict[str, Any]:
|
|
471
|
+
"""
|
|
472
|
+
Check if text contains substring.
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
text: Text to search in
|
|
476
|
+
substring: String to find
|
|
477
|
+
case_sensitive: Case sensitive search
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
Dict with result
|
|
481
|
+
"""
|
|
482
|
+
try:
|
|
483
|
+
if case_sensitive:
|
|
484
|
+
found = substring in text
|
|
485
|
+
count = text.count(substring)
|
|
486
|
+
else:
|
|
487
|
+
found = substring.lower() in text.lower()
|
|
488
|
+
count = text.lower().count(substring.lower())
|
|
489
|
+
|
|
490
|
+
return {
|
|
491
|
+
"success": True,
|
|
492
|
+
"contains": found,
|
|
493
|
+
"count": count
|
|
494
|
+
}
|
|
495
|
+
except Exception as e:
|
|
496
|
+
return {"success": False, "error": str(e)}
|
|
497
|
+
|
|
498
|
+
@tool(
|
|
499
|
+
name="text_truncate",
|
|
500
|
+
description="Truncate text to a maximum length",
|
|
501
|
+
tags=["text", "truncate", "safe"]
|
|
502
|
+
)
|
|
503
|
+
def truncate(
|
|
504
|
+
self,
|
|
505
|
+
text: str,
|
|
506
|
+
max_length: int,
|
|
507
|
+
suffix: str = "..."
|
|
508
|
+
) -> Dict[str, Any]:
|
|
509
|
+
"""
|
|
510
|
+
Truncate text with suffix.
|
|
511
|
+
|
|
512
|
+
Args:
|
|
513
|
+
text: Text to truncate
|
|
514
|
+
max_length: Maximum length
|
|
515
|
+
suffix: Suffix to add if truncated
|
|
516
|
+
|
|
517
|
+
Returns:
|
|
518
|
+
Dict with result
|
|
519
|
+
"""
|
|
520
|
+
try:
|
|
521
|
+
if len(text) <= max_length:
|
|
522
|
+
return {
|
|
523
|
+
"success": True,
|
|
524
|
+
"result": text,
|
|
525
|
+
"truncated": False
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
result = text[:max_length - len(suffix)] + suffix
|
|
529
|
+
|
|
530
|
+
return {
|
|
531
|
+
"success": True,
|
|
532
|
+
"result": result,
|
|
533
|
+
"truncated": True,
|
|
534
|
+
"original_length": len(text)
|
|
535
|
+
}
|
|
536
|
+
except Exception as e:
|
|
537
|
+
return {"success": False, "error": str(e)}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""
|
|
4
|
+
Safe Toolkit Factory.
|
|
5
|
+
|
|
6
|
+
Creates pre-configured collections of safe tools for common use cases.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Dict, List, Optional, Any
|
|
10
|
+
|
|
11
|
+
from atr.tools.safe.http_client import HttpClientTool
|
|
12
|
+
from atr.tools.safe.file_reader import FileReaderTool
|
|
13
|
+
from atr.tools.safe.json_parser import JsonParserTool
|
|
14
|
+
from atr.tools.safe.calculator import CalculatorTool
|
|
15
|
+
from atr.tools.safe.datetime_tool import DateTimeTool
|
|
16
|
+
from atr.tools.safe.text_tool import TextTool
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def create_safe_toolkit(
|
|
20
|
+
preset: str = "standard",
|
|
21
|
+
config: Optional[Dict[str, Any]] = None
|
|
22
|
+
) -> Dict[str, Any]:
|
|
23
|
+
"""
|
|
24
|
+
Create a pre-configured toolkit of safe tools.
|
|
25
|
+
|
|
26
|
+
Presets:
|
|
27
|
+
- "minimal": Only calculator, datetime, text (no I/O)
|
|
28
|
+
- "standard": All tools with sensible defaults
|
|
29
|
+
- "restricted": Standard but with more restrictions
|
|
30
|
+
- "readonly": File reader + JSON parser + calculator
|
|
31
|
+
- "network": HTTP client only
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
preset: Preset name
|
|
35
|
+
config: Optional configuration overrides
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Dict with tool instances and registry helper
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
```python
|
|
42
|
+
# Create standard toolkit
|
|
43
|
+
toolkit = create_safe_toolkit("standard")
|
|
44
|
+
|
|
45
|
+
# Access tools
|
|
46
|
+
http = toolkit["http"]
|
|
47
|
+
files = toolkit["files"]
|
|
48
|
+
|
|
49
|
+
# Register all tools with ATR
|
|
50
|
+
from atr import ToolRegistry
|
|
51
|
+
registry = ToolRegistry()
|
|
52
|
+
toolkit["register_all"](registry)
|
|
53
|
+
```
|
|
54
|
+
"""
|
|
55
|
+
config = config or {}
|
|
56
|
+
|
|
57
|
+
tools = {}
|
|
58
|
+
|
|
59
|
+
if preset == "minimal":
|
|
60
|
+
# No I/O tools - just processing
|
|
61
|
+
tools["calculator"] = CalculatorTool(
|
|
62
|
+
precision=config.get("precision", 15)
|
|
63
|
+
)
|
|
64
|
+
tools["datetime"] = DateTimeTool(
|
|
65
|
+
default_timezone=config.get("timezone", "UTC")
|
|
66
|
+
)
|
|
67
|
+
tools["text"] = TextTool(
|
|
68
|
+
max_length=config.get("max_text_length", 100000)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
elif preset == "readonly":
|
|
72
|
+
# File reading and processing
|
|
73
|
+
tools["files"] = FileReaderTool(
|
|
74
|
+
sandbox_paths=config.get("sandbox_paths"),
|
|
75
|
+
allowed_extensions=config.get("allowed_extensions", [
|
|
76
|
+
".txt", ".json", ".yaml", ".yml", ".md", ".csv", ".xml"
|
|
77
|
+
]),
|
|
78
|
+
max_file_size=config.get("max_file_size", 1_000_000)
|
|
79
|
+
)
|
|
80
|
+
tools["json"] = JsonParserTool(
|
|
81
|
+
max_size=config.get("max_json_size", 1_000_000)
|
|
82
|
+
)
|
|
83
|
+
tools["calculator"] = CalculatorTool()
|
|
84
|
+
tools["datetime"] = DateTimeTool()
|
|
85
|
+
tools["text"] = TextTool()
|
|
86
|
+
|
|
87
|
+
elif preset == "network":
|
|
88
|
+
# HTTP only
|
|
89
|
+
tools["http"] = HttpClientTool(
|
|
90
|
+
allowed_domains=config.get("allowed_domains"),
|
|
91
|
+
blocked_domains=config.get("blocked_domains"),
|
|
92
|
+
rate_limit=config.get("rate_limit", 60),
|
|
93
|
+
timeout=config.get("timeout", 30),
|
|
94
|
+
max_response_size=config.get("max_response_size", 10_000_000)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
elif preset == "restricted":
|
|
98
|
+
# All tools with stricter limits
|
|
99
|
+
tools["http"] = HttpClientTool(
|
|
100
|
+
allowed_domains=config.get("allowed_domains", []), # Must specify domains
|
|
101
|
+
rate_limit=config.get("rate_limit", 10), # Lower rate limit
|
|
102
|
+
timeout=config.get("timeout", 10),
|
|
103
|
+
max_response_size=config.get("max_response_size", 1_000_000)
|
|
104
|
+
)
|
|
105
|
+
tools["files"] = FileReaderTool(
|
|
106
|
+
sandbox_paths=config.get("sandbox_paths", []), # Must specify paths
|
|
107
|
+
allowed_extensions=config.get("allowed_extensions", [".txt", ".json"]),
|
|
108
|
+
max_file_size=config.get("max_file_size", 100_000),
|
|
109
|
+
follow_symlinks=False
|
|
110
|
+
)
|
|
111
|
+
tools["json"] = JsonParserTool(
|
|
112
|
+
max_size=config.get("max_json_size", 100_000),
|
|
113
|
+
max_depth=config.get("max_depth", 20)
|
|
114
|
+
)
|
|
115
|
+
tools["calculator"] = CalculatorTool(precision=10)
|
|
116
|
+
tools["datetime"] = DateTimeTool()
|
|
117
|
+
tools["text"] = TextTool(max_length=10000)
|
|
118
|
+
|
|
119
|
+
else: # "standard" or default
|
|
120
|
+
# All tools with sensible defaults
|
|
121
|
+
tools["http"] = HttpClientTool(
|
|
122
|
+
allowed_domains=config.get("allowed_domains"),
|
|
123
|
+
blocked_domains=config.get("blocked_domains"),
|
|
124
|
+
rate_limit=config.get("rate_limit", 60),
|
|
125
|
+
timeout=config.get("timeout", 30),
|
|
126
|
+
max_response_size=config.get("max_response_size", 10_000_000)
|
|
127
|
+
)
|
|
128
|
+
tools["files"] = FileReaderTool(
|
|
129
|
+
sandbox_paths=config.get("sandbox_paths"),
|
|
130
|
+
allowed_extensions=config.get("allowed_extensions"),
|
|
131
|
+
max_file_size=config.get("max_file_size", 10_000_000)
|
|
132
|
+
)
|
|
133
|
+
tools["json"] = JsonParserTool(
|
|
134
|
+
max_size=config.get("max_json_size", 10_000_000)
|
|
135
|
+
)
|
|
136
|
+
tools["calculator"] = CalculatorTool(
|
|
137
|
+
precision=config.get("precision", 15)
|
|
138
|
+
)
|
|
139
|
+
tools["datetime"] = DateTimeTool(
|
|
140
|
+
default_timezone=config.get("timezone", "UTC")
|
|
141
|
+
)
|
|
142
|
+
tools["text"] = TextTool(
|
|
143
|
+
max_length=config.get("max_text_length", 1_000_000)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Helper function to register all tools
|
|
147
|
+
def register_all(registry):
|
|
148
|
+
"""Register all tools with an ATR registry."""
|
|
149
|
+
for name, tool in tools.items():
|
|
150
|
+
if name.startswith("_"):
|
|
151
|
+
continue
|
|
152
|
+
# Register tool methods
|
|
153
|
+
for method_name in dir(tool):
|
|
154
|
+
if method_name.startswith("_"):
|
|
155
|
+
continue
|
|
156
|
+
method = getattr(tool, method_name)
|
|
157
|
+
if callable(method) and hasattr(method, "_tool_metadata"):
|
|
158
|
+
registry.register(method)
|
|
159
|
+
|
|
160
|
+
tools["register_all"] = register_all
|
|
161
|
+
tools["_preset"] = preset
|
|
162
|
+
tools["_config"] = config
|
|
163
|
+
|
|
164
|
+
return tools
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def list_presets() -> Dict[str, str]:
|
|
168
|
+
"""List available toolkit presets."""
|
|
169
|
+
return {
|
|
170
|
+
"minimal": "Calculator, datetime, text - no I/O operations",
|
|
171
|
+
"standard": "All tools with sensible defaults",
|
|
172
|
+
"restricted": "All tools with stricter security limits",
|
|
173
|
+
"readonly": "File reader, JSON parser, calculator - no network",
|
|
174
|
+
"network": "HTTP client only - no file access"
|
|
175
|
+
}
|