subactor-shell 0.2.2__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.
- subactor_shell/__init__.py +3 -0
- subactor_shell/__main__.py +4 -0
- subactor_shell/acp_agent.py +363 -0
- subactor_shell/app.py +482 -0
- subactor_shell/artifacts.py +198 -0
- subactor_shell/catalog.py +416 -0
- subactor_shell/chat.py +517 -0
- subactor_shell/compiler.py +191 -0
- subactor_shell/config.py +374 -0
- subactor_shell/connectors.py +503 -0
- subactor_shell/context_builder.py +153 -0
- subactor_shell/control.py +141 -0
- subactor_shell/control_env.py +93 -0
- subactor_shell/intent_ir.py +187 -0
- subactor_shell/models.py +78 -0
- subactor_shell/operations.py +287 -0
- subactor_shell/orchestration.py +324 -0
- subactor_shell/policy.py +38 -0
- subactor_shell/providers/__init__.py +63 -0
- subactor_shell/providers/anthropic.py +101 -0
- subactor_shell/providers/base.py +81 -0
- subactor_shell/providers/mock.py +32 -0
- subactor_shell/providers/openai_compat.py +303 -0
- subactor_shell/providers/subactor_control.py +191 -0
- subactor_shell/redaction.py +62 -0
- subactor_shell/repl.py +480 -0
- subactor_shell/routing.py +334 -0
- subactor_shell/secret_refs.py +82 -0
- subactor_shell/store.py +857 -0
- subactor_shell/terminal.py +79 -0
- subactor_shell/token_budget.py +46 -0
- subactor_shell/vault.py +170 -0
- subactor_shell-0.2.2.dist-info/METADATA +449 -0
- subactor_shell-0.2.2.dist-info/RECORD +38 -0
- subactor_shell-0.2.2.dist-info/WHEEL +5 -0
- subactor_shell-0.2.2.dist-info/entry_points.txt +2 -0
- subactor_shell-0.2.2.dist-info/licenses/LICENSE +13 -0
- subactor_shell-0.2.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
from .catalog import CandidateMatch, CandidateRetriever, IntentCatalog, normalize_text
|
|
10
|
+
from .config import AppConfig
|
|
11
|
+
from .intent_ir import IntentIR, IntentValidationError, intent_ir_schema
|
|
12
|
+
from .models import Session
|
|
13
|
+
from .providers import ProviderBundle
|
|
14
|
+
from .providers.base import ProviderError
|
|
15
|
+
from .secret_refs import SecretResolver
|
|
16
|
+
from .store import Store
|
|
17
|
+
from .token_budget import TokenUsage
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
ProviderBuilder = Callable[[Any, SecretResolver], ProviderBundle]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(slots=True)
|
|
24
|
+
class ParserUsageRecord:
|
|
25
|
+
provider: str
|
|
26
|
+
model: str
|
|
27
|
+
purpose: str
|
|
28
|
+
usage: TokenUsage
|
|
29
|
+
latency_ms: int = 0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(slots=True)
|
|
33
|
+
class RoutingDecision:
|
|
34
|
+
route: str
|
|
35
|
+
reason: str
|
|
36
|
+
confidence: float = 0.0
|
|
37
|
+
intent: IntentIR | None = None
|
|
38
|
+
provider: str = ""
|
|
39
|
+
model: str = ""
|
|
40
|
+
candidates: list[CandidateMatch] = field(default_factory=list)
|
|
41
|
+
parser_usage: list[ParserUsageRecord] = field(default_factory=list)
|
|
42
|
+
parser_errors: list[str] = field(default_factory=list)
|
|
43
|
+
cache_hit: bool = False
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def intent_id(self) -> str:
|
|
47
|
+
return self.intent.intent_id if self.intent else ""
|
|
48
|
+
|
|
49
|
+
def route_context(self) -> dict[str, Any]:
|
|
50
|
+
return {
|
|
51
|
+
"route": self.route,
|
|
52
|
+
"reason": self.reason,
|
|
53
|
+
"confidence": round(self.confidence, 4),
|
|
54
|
+
"intent": self.intent.to_dict() if self.intent else None,
|
|
55
|
+
"candidates": [
|
|
56
|
+
{
|
|
57
|
+
"intent_id": item.intent.id,
|
|
58
|
+
"score": round(item.score, 4),
|
|
59
|
+
"required_args": item.intent.required_args,
|
|
60
|
+
"description": item.intent.description[:160],
|
|
61
|
+
}
|
|
62
|
+
for item in self.candidates[:5]
|
|
63
|
+
],
|
|
64
|
+
"parser_errors": self.parser_errors[-3:],
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Router:
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
config: AppConfig,
|
|
72
|
+
store: Store,
|
|
73
|
+
resolver: SecretResolver,
|
|
74
|
+
provider_builder: ProviderBuilder,
|
|
75
|
+
catalog: IntentCatalog,
|
|
76
|
+
):
|
|
77
|
+
self.config = config
|
|
78
|
+
self.store = store
|
|
79
|
+
self.resolver = resolver
|
|
80
|
+
self.provider_builder = provider_builder
|
|
81
|
+
self.catalog = catalog
|
|
82
|
+
self.retriever = CandidateRetriever(catalog)
|
|
83
|
+
options = config.orchestration
|
|
84
|
+
self.enabled = bool(options.get("enabled", True))
|
|
85
|
+
self.mode = str(options.get("mode", "active")).strip().lower()
|
|
86
|
+
self.top_k = max(1, int(options.get("top_k", 5)))
|
|
87
|
+
self.min_candidate_score = float(options.get("min_candidate_score", 0.32))
|
|
88
|
+
self.deterministic_threshold = float(options.get("deterministic_threshold", 0.93))
|
|
89
|
+
self.local_threshold = float(options.get("local_execute_threshold", 0.82))
|
|
90
|
+
self.cheap_threshold = float(options.get("cheap_remote_threshold", 0.68))
|
|
91
|
+
self.max_output_tokens = max(64, int(options.get("max_parser_output_tokens", 192)))
|
|
92
|
+
self.cache_ttl = max(1, int(options.get("cache_ttl_seconds", 86_400)))
|
|
93
|
+
self.local_provider = str(options.get("local_parser_provider", "")).strip()
|
|
94
|
+
self.local_model = str(options.get("local_parser_model", "")).strip()
|
|
95
|
+
self.cheap_provider = str(options.get("cheap_parser_provider", "")).strip()
|
|
96
|
+
self.cheap_model = str(options.get("cheap_parser_model", "")).strip()
|
|
97
|
+
self.large_provider = str(options.get("large_provider", "")).strip()
|
|
98
|
+
self.large_model = str(options.get("large_model", "")).strip()
|
|
99
|
+
|
|
100
|
+
async def route(self, session: Session, text: str, *, cancel_event=None) -> RoutingDecision:
|
|
101
|
+
if not self.enabled or self.mode == "off":
|
|
102
|
+
return RoutingDecision(
|
|
103
|
+
route="chat_provider",
|
|
104
|
+
reason="Orkiestracja DSL jest wyłączona",
|
|
105
|
+
provider=session.provider,
|
|
106
|
+
model=session.model,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
candidates = self.retriever.retrieve(text, top_k=self.top_k)
|
|
110
|
+
top_score = candidates[0].score if candidates else 0.0
|
|
111
|
+
margin = top_score - (candidates[1].score if len(candidates) > 1 else 0.0)
|
|
112
|
+
cache_key = self._cache_key(text)
|
|
113
|
+
cached = self.store.cache_get(cache_key)
|
|
114
|
+
if cached:
|
|
115
|
+
definition = self.catalog.get(str(cached.get("intent_id", "")))
|
|
116
|
+
if definition:
|
|
117
|
+
try:
|
|
118
|
+
intent = definition.validate_ir(cached)
|
|
119
|
+
return RoutingDecision(
|
|
120
|
+
route="cache",
|
|
121
|
+
reason="Powtórzone polecenie odtworzono z cache walidowanego IntentIR",
|
|
122
|
+
confidence=float(cached.get("_confidence", 0.95)),
|
|
123
|
+
intent=intent,
|
|
124
|
+
candidates=candidates,
|
|
125
|
+
cache_hit=True,
|
|
126
|
+
)
|
|
127
|
+
except IntentValidationError:
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
deterministic = self._deterministic(candidates)
|
|
131
|
+
if deterministic:
|
|
132
|
+
intent, confidence = deterministic
|
|
133
|
+
self._cache(cache_key, intent, confidence)
|
|
134
|
+
return RoutingDecision(
|
|
135
|
+
route="deterministic",
|
|
136
|
+
reason="Exact/template phrase match oraz lokalna walidacja argumentów",
|
|
137
|
+
confidence=confidence,
|
|
138
|
+
intent=intent,
|
|
139
|
+
candidates=candidates,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if not candidates or top_score < self.min_candidate_score:
|
|
143
|
+
return RoutingDecision(
|
|
144
|
+
route="chat_provider",
|
|
145
|
+
reason="Brak wystarczająco bliskiego intentu w lokalnym katalogu",
|
|
146
|
+
confidence=top_score,
|
|
147
|
+
provider=self.large_provider or session.provider,
|
|
148
|
+
model=self.large_model or session.model,
|
|
149
|
+
candidates=candidates,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
attempts: list[ParserUsageRecord] = []
|
|
153
|
+
errors: list[str] = []
|
|
154
|
+
configured: list[tuple[str, str, str, float]] = []
|
|
155
|
+
seen: set[tuple[str, str]] = set()
|
|
156
|
+
for route, provider_name, model_name, threshold in (
|
|
157
|
+
("local_4b", self.local_provider, self.local_model, self.local_threshold),
|
|
158
|
+
("cheap_remote", self.cheap_provider, self.cheap_model, self.cheap_threshold),
|
|
159
|
+
("large_remote", self.large_provider, self.large_model, 0.50),
|
|
160
|
+
):
|
|
161
|
+
if not provider_name:
|
|
162
|
+
continue
|
|
163
|
+
profile = self.config.provider(provider_name)
|
|
164
|
+
model = model_name or profile.model
|
|
165
|
+
key = (provider_name, model)
|
|
166
|
+
if key in seen:
|
|
167
|
+
continue
|
|
168
|
+
seen.add(key)
|
|
169
|
+
configured.append((route, provider_name, model, threshold))
|
|
170
|
+
|
|
171
|
+
for route, provider_name, model_name, threshold in configured:
|
|
172
|
+
intent, usage_record, error, confidence = await self._parse_with_provider(
|
|
173
|
+
text,
|
|
174
|
+
candidates,
|
|
175
|
+
route=route,
|
|
176
|
+
provider_name=provider_name,
|
|
177
|
+
model_name=model_name,
|
|
178
|
+
top_score=top_score,
|
|
179
|
+
margin=margin,
|
|
180
|
+
)
|
|
181
|
+
if usage_record:
|
|
182
|
+
attempts.append(usage_record)
|
|
183
|
+
if error:
|
|
184
|
+
errors.append(error)
|
|
185
|
+
if intent is not None and confidence >= threshold:
|
|
186
|
+
self._cache(cache_key, intent, confidence)
|
|
187
|
+
return RoutingDecision(
|
|
188
|
+
route=route,
|
|
189
|
+
reason="Poprawny IntentIR z krótkiej listy kandydatów i walidacji lokalnej",
|
|
190
|
+
confidence=self._calibrated(intent.intent_id, route, confidence),
|
|
191
|
+
intent=intent,
|
|
192
|
+
provider=provider_name,
|
|
193
|
+
model=model_name,
|
|
194
|
+
candidates=candidates,
|
|
195
|
+
parser_usage=attempts,
|
|
196
|
+
parser_errors=errors,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return RoutingDecision(
|
|
200
|
+
route="chat_provider",
|
|
201
|
+
reason="Parsery DSL nie zwróciły pewnego IntentIR; fallback do rozmowy z ograniczonym kontekstem",
|
|
202
|
+
confidence=top_score,
|
|
203
|
+
provider=self.large_provider or session.provider,
|
|
204
|
+
model=self.large_model or session.model,
|
|
205
|
+
candidates=candidates,
|
|
206
|
+
parser_usage=attempts,
|
|
207
|
+
parser_errors=errors,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def _deterministic(self, candidates: list[CandidateMatch]) -> tuple[IntentIR, float] | None:
|
|
211
|
+
if not candidates:
|
|
212
|
+
return None
|
|
213
|
+
candidate = candidates[0]
|
|
214
|
+
if not candidate.exact and candidate.score < self.deterministic_threshold:
|
|
215
|
+
return None
|
|
216
|
+
definition = candidate.intent
|
|
217
|
+
effect = str(definition.execution.get("effect", "read"))
|
|
218
|
+
mode = "execute" if effect == "read" else "plan"
|
|
219
|
+
ir = IntentIR.from_dict(
|
|
220
|
+
{
|
|
221
|
+
"v": 1,
|
|
222
|
+
"intent_id": definition.id,
|
|
223
|
+
"mode": mode,
|
|
224
|
+
"args": {**definition.defaults, **candidate.extracted_args},
|
|
225
|
+
"requirements": [],
|
|
226
|
+
"constraints": definition.constraints,
|
|
227
|
+
"unresolved": [],
|
|
228
|
+
}
|
|
229
|
+
)
|
|
230
|
+
ir = definition.validate_ir(ir)
|
|
231
|
+
confidence = 0.99 if candidate.exact else candidate.score
|
|
232
|
+
if ir.unresolved:
|
|
233
|
+
confidence = min(confidence, 0.69)
|
|
234
|
+
return ir, confidence
|
|
235
|
+
|
|
236
|
+
async def _parse_with_provider(
|
|
237
|
+
self,
|
|
238
|
+
text: str,
|
|
239
|
+
candidates: list[CandidateMatch],
|
|
240
|
+
*,
|
|
241
|
+
route: str,
|
|
242
|
+
provider_name: str,
|
|
243
|
+
model_name: str,
|
|
244
|
+
top_score: float,
|
|
245
|
+
margin: float,
|
|
246
|
+
) -> tuple[IntentIR | None, ParserUsageRecord | None, str, float]:
|
|
247
|
+
started = time.perf_counter()
|
|
248
|
+
try:
|
|
249
|
+
profile = self.config.provider(provider_name)
|
|
250
|
+
bundle = self.provider_builder(profile, self.resolver)
|
|
251
|
+
except (KeyError, ValueError, ProviderError) as exc:
|
|
252
|
+
return None, None, f"{route}: nie można uruchomić providera ({exc})", 0.0
|
|
253
|
+
|
|
254
|
+
shortlist = [
|
|
255
|
+
{
|
|
256
|
+
"intent_id": item.intent.id,
|
|
257
|
+
"description": item.intent.description,
|
|
258
|
+
"required_args": item.intent.required_args,
|
|
259
|
+
"optional_args": item.intent.optional_args,
|
|
260
|
+
"defaults": item.intent.defaults,
|
|
261
|
+
"risk": item.intent.risk,
|
|
262
|
+
}
|
|
263
|
+
for item in candidates
|
|
264
|
+
]
|
|
265
|
+
prompt = {
|
|
266
|
+
"user_text": text,
|
|
267
|
+
"candidate_intents": shortlist,
|
|
268
|
+
"rules": [
|
|
269
|
+
"Wybierz wyłącznie intent_id z candidate_intents.",
|
|
270
|
+
"Nie twórz command, shell, connector, endpoint ani sekretów.",
|
|
271
|
+
"args może zawierać wyłącznie pola zadeklarowane przez wybrany intent.",
|
|
272
|
+
"Brakujące wymagane pola wpisz do unresolved.",
|
|
273
|
+
"Dla odczytu użyj execute; dla zmiany stanu użyj plan.",
|
|
274
|
+
],
|
|
275
|
+
}
|
|
276
|
+
messages = [
|
|
277
|
+
{
|
|
278
|
+
"role": "user",
|
|
279
|
+
"content": json.dumps(prompt, ensure_ascii=False, sort_keys=True, separators=(",", ":")),
|
|
280
|
+
}
|
|
281
|
+
]
|
|
282
|
+
try:
|
|
283
|
+
completion = await bundle.provider.complete_structured(
|
|
284
|
+
messages,
|
|
285
|
+
model=model_name,
|
|
286
|
+
json_schema=intent_ir_schema([item.intent.id for item in candidates]),
|
|
287
|
+
schema_name="subactor_intent_ir_v1",
|
|
288
|
+
max_output_tokens=self.max_output_tokens,
|
|
289
|
+
reasoning_effort=profile.reasoning_effort or None,
|
|
290
|
+
)
|
|
291
|
+
definition = self.catalog.get(str(completion.data.get("intent_id", "")))
|
|
292
|
+
allowed_ids = {item.intent.id for item in candidates}
|
|
293
|
+
if not definition or definition.id not in allowed_ids:
|
|
294
|
+
raise IntentValidationError("Model wybrał intent spoza shortlisty")
|
|
295
|
+
intent = definition.validate_ir(completion.data)
|
|
296
|
+
confidence = min(0.97, 0.57 + 0.28 * top_score + 0.15 * max(0.0, margin))
|
|
297
|
+
if intent.unresolved:
|
|
298
|
+
confidence = min(confidence, 0.69)
|
|
299
|
+
record = ParserUsageRecord(
|
|
300
|
+
provider=provider_name,
|
|
301
|
+
model=model_name,
|
|
302
|
+
purpose=f"intent_parser:{route}",
|
|
303
|
+
usage=completion.usage,
|
|
304
|
+
latency_ms=int((time.perf_counter() - started) * 1000),
|
|
305
|
+
)
|
|
306
|
+
return intent, record, "", confidence
|
|
307
|
+
except (ProviderError, IntentValidationError, ValueError, KeyError) as exc:
|
|
308
|
+
return (
|
|
309
|
+
None,
|
|
310
|
+
ParserUsageRecord(
|
|
311
|
+
provider=provider_name,
|
|
312
|
+
model=model_name,
|
|
313
|
+
purpose=f"intent_parser:{route}",
|
|
314
|
+
usage=TokenUsage(),
|
|
315
|
+
latency_ms=int((time.perf_counter() - started) * 1000),
|
|
316
|
+
),
|
|
317
|
+
f"{route}: {exc}",
|
|
318
|
+
0.0,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
def _calibrated(self, intent_id: str, route: str, base: float) -> float:
|
|
322
|
+
historical = self.store.historical_success(intent_id, route)
|
|
323
|
+
if historical is None:
|
|
324
|
+
return base
|
|
325
|
+
return max(0.0, min(1.0, 0.75 * base + 0.25 * historical))
|
|
326
|
+
|
|
327
|
+
def _cache_key(self, text: str) -> str:
|
|
328
|
+
payload = normalize_text(text) + "\0" + self.catalog.fingerprint
|
|
329
|
+
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
330
|
+
|
|
331
|
+
def _cache(self, key: str, intent: IntentIR, confidence: float) -> None:
|
|
332
|
+
payload = intent.to_dict()
|
|
333
|
+
payload["_confidence"] = confidence
|
|
334
|
+
self.store.cache_set(key, payload, self.cache_ttl)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from urllib.parse import unquote
|
|
7
|
+
|
|
8
|
+
from .vault import VaultClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SecretRefError(RuntimeError):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_ENV_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SecretResolver:
|
|
19
|
+
def __init__(self, vault_config: dict, *, vault_transport=None):
|
|
20
|
+
self.vault_config = vault_config
|
|
21
|
+
self._vault_transport = vault_transport
|
|
22
|
+
self._vault_client: VaultClient | None = None
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def _read_env(reference: str) -> str:
|
|
26
|
+
name = reference.removeprefix("env://")
|
|
27
|
+
if not _ENV_NAME.fullmatch(name):
|
|
28
|
+
raise SecretRefError("Nieprawidłowa referencja env://")
|
|
29
|
+
value = os.environ.get(name)
|
|
30
|
+
if value is None:
|
|
31
|
+
raise SecretRefError(f"Brak zmiennej środowiskowej {name}")
|
|
32
|
+
return value
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def _file_path(reference: str) -> Path:
|
|
36
|
+
remainder = unquote(reference.removeprefix("file://"))
|
|
37
|
+
if not remainder or "?" in remainder or "#" in remainder:
|
|
38
|
+
raise SecretRefError("Nieprawidłowa referencja file://")
|
|
39
|
+
return Path(remainder).expanduser()
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def _read_file(cls, reference: str) -> str:
|
|
43
|
+
path = cls._file_path(reference)
|
|
44
|
+
try:
|
|
45
|
+
stat = path.stat()
|
|
46
|
+
if not path.is_file():
|
|
47
|
+
raise SecretRefError(f"Referencja nie wskazuje pliku: {path}")
|
|
48
|
+
if stat.st_size > 1024 * 1024:
|
|
49
|
+
raise SecretRefError("Plik sekretu jest większy niż 1 MiB")
|
|
50
|
+
return path.read_text(encoding="utf-8").rstrip("\r\n")
|
|
51
|
+
except OSError as exc:
|
|
52
|
+
raise SecretRefError(f"Nie można odczytać pliku sekretu: {path}") from exc
|
|
53
|
+
|
|
54
|
+
def resolve_without_vault(self, reference: str) -> str:
|
|
55
|
+
if reference.startswith("env://"):
|
|
56
|
+
return self._read_env(reference)
|
|
57
|
+
if reference.startswith("file://"):
|
|
58
|
+
return self._read_file(reference)
|
|
59
|
+
if reference.startswith("vault://"):
|
|
60
|
+
raise SecretRefError("Token dostępu do Vault nie może sam pochodzić z Vault")
|
|
61
|
+
raise SecretRefError("Obsługiwane referencje to env://, file:// i vault://")
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def vault(self) -> VaultClient:
|
|
65
|
+
if self._vault_client is None:
|
|
66
|
+
token_ref = str(self.vault_config.get("token_ref", "env://VAULT_TOKEN"))
|
|
67
|
+
self._vault_client = VaultClient(
|
|
68
|
+
address=str(self.vault_config.get("address", "http://127.0.0.1:8200")),
|
|
69
|
+
token_loader=lambda: self.resolve_without_vault(token_ref),
|
|
70
|
+
namespace=str(self.vault_config.get("namespace", "")),
|
|
71
|
+
verify_tls=bool(self.vault_config.get("verify_tls", True)),
|
|
72
|
+
timeout_seconds=float(self.vault_config.get("timeout_seconds", 10.0)),
|
|
73
|
+
transport=self._vault_transport,
|
|
74
|
+
)
|
|
75
|
+
return self._vault_client
|
|
76
|
+
|
|
77
|
+
def resolve(self, reference: str) -> str:
|
|
78
|
+
if reference.startswith("env://") or reference.startswith("file://"):
|
|
79
|
+
return self.resolve_without_vault(reference)
|
|
80
|
+
if reference.startswith("vault://"):
|
|
81
|
+
return self.vault.read_field(reference)
|
|
82
|
+
raise SecretRefError("Obsługiwane referencje to env://, file:// i vault://")
|