delimit-cli 2.3.2 → 3.0.0

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 (113) hide show
  1. package/.dockerignore +7 -0
  2. package/.github/workflows/ci.yml +22 -0
  3. package/CHANGELOG.md +33 -0
  4. package/CODE_OF_CONDUCT.md +48 -0
  5. package/CONTRIBUTING.md +67 -0
  6. package/Dockerfile +9 -0
  7. package/LICENSE +21 -0
  8. package/README.md +51 -130
  9. package/SECURITY.md +42 -0
  10. package/adapters/codex-forge.js +107 -0
  11. package/adapters/codex-jamsons.js +142 -0
  12. package/adapters/codex-security.js +94 -0
  13. package/adapters/gemini-forge.js +120 -0
  14. package/adapters/gemini-jamsons.js +152 -0
  15. package/bin/delimit-cli.js +52 -2
  16. package/bin/delimit-setup.js +258 -0
  17. package/gateway/ai/backends/__init__.py +0 -0
  18. package/gateway/ai/backends/async_utils.py +21 -0
  19. package/gateway/ai/backends/deploy_bridge.py +150 -0
  20. package/gateway/ai/backends/gateway_core.py +261 -0
  21. package/gateway/ai/backends/generate_bridge.py +38 -0
  22. package/gateway/ai/backends/governance_bridge.py +196 -0
  23. package/gateway/ai/backends/intel_bridge.py +59 -0
  24. package/gateway/ai/backends/memory_bridge.py +93 -0
  25. package/gateway/ai/backends/ops_bridge.py +137 -0
  26. package/gateway/ai/backends/os_bridge.py +82 -0
  27. package/gateway/ai/backends/repo_bridge.py +117 -0
  28. package/gateway/ai/backends/ui_bridge.py +118 -0
  29. package/gateway/ai/backends/vault_bridge.py +129 -0
  30. package/gateway/ai/server.py +1182 -0
  31. package/gateway/core/__init__.py +3 -0
  32. package/gateway/core/__pycache__/__init__.cpython-310.pyc +0 -0
  33. package/gateway/core/__pycache__/auto_baseline.cpython-310.pyc +0 -0
  34. package/gateway/core/__pycache__/ci_formatter.cpython-310.pyc +0 -0
  35. package/gateway/core/__pycache__/contract_ledger.cpython-310.pyc +0 -0
  36. package/gateway/core/__pycache__/dependency_graph.cpython-310.pyc +0 -0
  37. package/gateway/core/__pycache__/dependency_manifest.cpython-310.pyc +0 -0
  38. package/gateway/core/__pycache__/diff_engine_v2.cpython-310.pyc +0 -0
  39. package/gateway/core/__pycache__/event_backbone.cpython-310.pyc +0 -0
  40. package/gateway/core/__pycache__/event_schema.cpython-310.pyc +0 -0
  41. package/gateway/core/__pycache__/explainer.cpython-310.pyc +0 -0
  42. package/gateway/core/__pycache__/gateway.cpython-310.pyc +0 -0
  43. package/gateway/core/__pycache__/gateway_v2.cpython-310.pyc +0 -0
  44. package/gateway/core/__pycache__/gateway_v3.cpython-310.pyc +0 -0
  45. package/gateway/core/__pycache__/impact_analyzer.cpython-310.pyc +0 -0
  46. package/gateway/core/__pycache__/policy_engine.cpython-310.pyc +0 -0
  47. package/gateway/core/__pycache__/registry.cpython-310.pyc +0 -0
  48. package/gateway/core/__pycache__/registry_v2.cpython-310.pyc +0 -0
  49. package/gateway/core/__pycache__/registry_v3.cpython-310.pyc +0 -0
  50. package/gateway/core/__pycache__/semver_classifier.cpython-310.pyc +0 -0
  51. package/gateway/core/__pycache__/spec_detector.cpython-310.pyc +0 -0
  52. package/gateway/core/__pycache__/surface_bridge.cpython-310.pyc +0 -0
  53. package/gateway/core/auto_baseline.py +304 -0
  54. package/gateway/core/ci_formatter.py +283 -0
  55. package/gateway/core/complexity_analyzer.py +386 -0
  56. package/gateway/core/contract_ledger.py +345 -0
  57. package/gateway/core/dependency_graph.py +218 -0
  58. package/gateway/core/dependency_manifest.py +223 -0
  59. package/gateway/core/diff_engine_v2.py +477 -0
  60. package/gateway/core/diff_engine_v2.py.bak +426 -0
  61. package/gateway/core/event_backbone.py +268 -0
  62. package/gateway/core/event_schema.py +258 -0
  63. package/gateway/core/explainer.py +438 -0
  64. package/gateway/core/gateway.py +128 -0
  65. package/gateway/core/gateway_v2.py +154 -0
  66. package/gateway/core/gateway_v3.py +224 -0
  67. package/gateway/core/impact_analyzer.py +163 -0
  68. package/gateway/core/policies/default.yml +13 -0
  69. package/gateway/core/policies/relaxed.yml +48 -0
  70. package/gateway/core/policies/strict.yml +55 -0
  71. package/gateway/core/policy_engine.py +464 -0
  72. package/gateway/core/registry.py +52 -0
  73. package/gateway/core/registry_v2.py +132 -0
  74. package/gateway/core/registry_v3.py +134 -0
  75. package/gateway/core/semver_classifier.py +152 -0
  76. package/gateway/core/spec_detector.py +130 -0
  77. package/gateway/core/surface_bridge.py +307 -0
  78. package/gateway/core/zero_spec/__init__.py +4 -0
  79. package/gateway/core/zero_spec/__pycache__/__init__.cpython-310.pyc +0 -0
  80. package/gateway/core/zero_spec/__pycache__/detector.cpython-310.pyc +0 -0
  81. package/gateway/core/zero_spec/__pycache__/express_extractor.cpython-310.pyc +0 -0
  82. package/gateway/core/zero_spec/__pycache__/fastapi_extractor.cpython-310.pyc +0 -0
  83. package/gateway/core/zero_spec/__pycache__/nestjs_extractor.cpython-310.pyc +0 -0
  84. package/gateway/core/zero_spec/detector.py +353 -0
  85. package/gateway/core/zero_spec/express_extractor.py +483 -0
  86. package/gateway/core/zero_spec/fastapi_extractor.py +254 -0
  87. package/gateway/core/zero_spec/nestjs_extractor.py +369 -0
  88. package/gateway/tasks/__init__.py +1 -0
  89. package/gateway/tasks/__pycache__/__init__.cpython-310.pyc +0 -0
  90. package/gateway/tasks/__pycache__/check_policy.cpython-310.pyc +0 -0
  91. package/gateway/tasks/__pycache__/check_policy_v2.cpython-310.pyc +0 -0
  92. package/gateway/tasks/__pycache__/check_policy_v3.cpython-310.pyc +0 -0
  93. package/gateway/tasks/__pycache__/explain_diff.cpython-310.pyc +0 -0
  94. package/gateway/tasks/__pycache__/explain_diff_v2.cpython-310.pyc +0 -0
  95. package/gateway/tasks/__pycache__/validate_api.cpython-310.pyc +0 -0
  96. package/gateway/tasks/__pycache__/validate_api_v2.cpython-310.pyc +0 -0
  97. package/gateway/tasks/__pycache__/validate_api_v3.cpython-310.pyc +0 -0
  98. package/gateway/tasks/check_policy.py +177 -0
  99. package/gateway/tasks/check_policy_v2.py +255 -0
  100. package/gateway/tasks/check_policy_v3.py +255 -0
  101. package/gateway/tasks/explain_diff.py +305 -0
  102. package/gateway/tasks/explain_diff_v2.py +267 -0
  103. package/gateway/tasks/validate_api.py +131 -0
  104. package/gateway/tasks/validate_api_v2.py +208 -0
  105. package/gateway/tasks/validate_api_v3.py +163 -0
  106. package/package.json +3 -3
  107. package/adapters/codex-skill.js +0 -87
  108. package/adapters/cursor-extension.js +0 -190
  109. package/adapters/gemini-action.js +0 -93
  110. package/adapters/openai-function.js +0 -112
  111. package/adapters/xai-plugin.js +0 -151
  112. package/test-decision-engine.js +0 -181
  113. package/test-hook.js +0 -27
@@ -0,0 +1,117 @@
1
+ """
2
+ Bridge to repo-level tools: repodoctor, configsentry, evidencepack, securitygate.
3
+ Tier 3 Extended — repository health, config audit, evidence, security.
4
+ """
5
+
6
+ import os
7
+ import sys
8
+ import json
9
+ import asyncio
10
+ import importlib
11
+ import logging
12
+ from pathlib import Path
13
+ from typing import Any, Dict, List, Optional
14
+ from .async_utils import run_async
15
+
16
+ logger = logging.getLogger("delimit.ai.repo_bridge")
17
+
18
+ PACKAGES = Path("/home/delimit/.delimit_suite/packages")
19
+
20
+ # Add PACKAGES dir so `from shared.base_server import BaseMCPServer` resolves
21
+ _packages = str(PACKAGES)
22
+ if _packages not in sys.path:
23
+ sys.path.insert(0, _packages)
24
+
25
+ _servers = {}
26
+
27
+
28
+ def _call(pkg: str, factory_name: str, method: str, args: Dict, tool_label: str) -> Dict[str, Any]:
29
+ try:
30
+ srv = _servers.get(pkg)
31
+ if srv is None:
32
+ mod = importlib.import_module(f"{pkg}.server")
33
+ factory = getattr(mod, factory_name)
34
+ srv = factory()
35
+ # Some servers need async initialization (e.g. evidencepack)
36
+ init_fn = getattr(srv, "initialize", None)
37
+ if init_fn and asyncio.iscoroutinefunction(init_fn):
38
+ run_async(init_fn())
39
+ _servers[pkg] = srv
40
+ fn = getattr(srv, method, None)
41
+ if fn is None:
42
+ return {"tool": tool_label, "status": "not_implemented", "error": f"Method {method} not found"}
43
+ result = run_async(fn(args, None))
44
+ return json.loads(result) if isinstance(result, str) else result
45
+ except Exception as e:
46
+ return {"tool": tool_label, "error": str(e)}
47
+
48
+
49
+ # ─── RepoDoctor ────────────────────────────────────────────────────────
50
+
51
+ def diagnose(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
52
+ return _call("repodoctor", "create_repodoctor_server", "_tool_health_check",
53
+ {"repository_path": target, **(options or {})}, "repo.diagnose")
54
+
55
+
56
+ def analyze(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
57
+ return _call("repodoctor", "create_repodoctor_server", "_tool_snapshot",
58
+ {"repository_path": target, **(options or {})}, "repo.analyze")
59
+
60
+
61
+ # ─── ConfigSentry ───────────────────────────────────────────────────────
62
+
63
+ def config_validate(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
64
+ return _call("configsentry", "create_configsentry_server", "_tool_validate",
65
+ {"repository_path": target, **(options or {})}, "config.validate")
66
+
67
+
68
+ def config_audit(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
69
+ return _call("configsentry", "create_configsentry_server", "_tool_env_audit",
70
+ {"repository_path": target, **(options or {})}, "config.audit")
71
+
72
+
73
+ # ─── EvidencePack ───────────────────────────────────────────────────────
74
+
75
+ def evidence_collect(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
76
+ result = _call("evidencepack", "create_evidencepack_server", "_tool_list",
77
+ {"limit": 20, **(options or {})}, "evidence.collect")
78
+ # Provide a clear message when no evidence bundles exist yet
79
+ if isinstance(result, dict) and result.get("total_bundles", -1) == 0:
80
+ result["message"] = (
81
+ "No evidence collected yet. Use evidence.begin to start a collection, "
82
+ "evidence.capture to add items, and evidence.finalize to create a bundle."
83
+ )
84
+ return result
85
+
86
+
87
+ def evidence_verify(bundle_id: Optional[str] = None, bundle_path: Optional[str] = None, options: Optional[Dict] = None) -> Dict[str, Any]:
88
+ args = {**(options or {})}
89
+ if bundle_id:
90
+ args["bundle_id"] = bundle_id
91
+ if bundle_path:
92
+ args["bundle_path"] = bundle_path
93
+ if not bundle_id and not bundle_path:
94
+ return {"tool": "evidence.verify", "status": "no_input", "message": "Provide bundle_id or bundle_path to verify"}
95
+ return _call("evidencepack", "create_evidencepack_server", "_tool_validate",
96
+ args, "evidence.verify")
97
+
98
+
99
+ # ─── SecurityGate ───────────────────────────────────────────────────────
100
+
101
+ _INTERNAL_TOKEN = os.environ.get("DELIMIT_INTERNAL_BRIDGE_TOKEN", "delimit-internal-bridge")
102
+
103
+
104
+ def security_scan(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
105
+ result = _call("securitygate", "create_securitygate_server", "_tool_scan",
106
+ {"target": target, "authorization_token": _INTERNAL_TOKEN, **(options or {})}, "security.scan")
107
+ # Guard against fabricated/hardcoded CVE data from stub implementations
108
+ vulns = result.get("vulnerabilities", [])
109
+ if vulns and any("CVE-2023-12345" in str(v.get("id", "")) for v in vulns):
110
+ return {"tool": "security.scan", "status": "not_available",
111
+ "error": "Security scanner returned placeholder data. Install a real vulnerability scanner."}
112
+ return result
113
+
114
+
115
+ def security_audit(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
116
+ return _call("securitygate", "create_securitygate_server", "_tool_audit",
117
+ {"target": target, "authorization_token": _INTERNAL_TOKEN, **(options or {})}, "security.audit")
@@ -0,0 +1,118 @@
1
+ """
2
+ Bridge to UI tooling: designsystem, storybook, testsmith, docsweaver.
3
+ UI/DX tools for component development and testing.
4
+ """
5
+
6
+ import sys
7
+ import json
8
+ import asyncio
9
+ import importlib
10
+ import logging
11
+ from pathlib import Path
12
+ from typing import Any, Dict, List, Optional
13
+ from .async_utils import run_async
14
+
15
+ logger = logging.getLogger("delimit.ai.ui_bridge")
16
+
17
+ PACKAGES = Path("/home/delimit/.delimit_suite/packages")
18
+
19
+ # Add PACKAGES dir so `from shared.base_server import BaseMCPServer` resolves
20
+ _packages = str(PACKAGES)
21
+ if _packages not in sys.path:
22
+ sys.path.insert(0, _packages)
23
+
24
+ _servers = {}
25
+
26
+
27
+ def _call(pkg: str, factory_name: str, method: str, args: Dict, tool_label: str) -> Dict[str, Any]:
28
+ """Call a _tool_* method on a BaseMCPServer-derived package."""
29
+ try:
30
+ srv = _servers.get(pkg)
31
+ if srv is None:
32
+ mod = importlib.import_module(f"{pkg}.server")
33
+ factory = getattr(mod, factory_name)
34
+ srv = factory()
35
+ _servers[pkg] = srv
36
+ fn = getattr(srv, method, None)
37
+ if fn is None:
38
+ return {"tool": tool_label, "status": "not_implemented", "error": f"Method {method} not found"}
39
+ result = run_async(fn(args, None))
40
+ return json.loads(result) if isinstance(result, str) else result
41
+ except Exception as e:
42
+ return {"tool": tool_label, "error": str(e)}
43
+
44
+
45
+ # ─── DesignSystem (custom classes, no BaseMCPServer) ───────────────────
46
+ # designsystem uses DesignSystemGenerator, not the _tool_* pattern.
47
+ # Provide graceful pass-through until refactored.
48
+
49
+ def design_validate_responsive(project_path: str, check_types: Optional[List[str]] = None) -> Dict[str, Any]:
50
+ return {"tool": "design.validate_responsive", "project_path": project_path, "status": "pass-through"}
51
+
52
+
53
+ def design_extract_tokens(figma_file_key: str, token_types: Optional[List[str]] = None) -> Dict[str, Any]:
54
+ return {"tool": "design.extract_tokens", "figma_file_key": figma_file_key, "status": "pass-through"}
55
+
56
+
57
+ def design_generate_component(component_name: str, figma_node_id: Optional[str] = None, output_path: Optional[str] = None) -> Dict[str, Any]:
58
+ return {"tool": "design.generate_component", "component_name": component_name, "status": "pass-through"}
59
+
60
+
61
+ def design_generate_tailwind(figma_file_key: str, output_path: Optional[str] = None) -> Dict[str, Any]:
62
+ return {"tool": "design.generate_tailwind", "figma_file_key": figma_file_key, "status": "pass-through"}
63
+
64
+
65
+ def design_component_library(project_path: str, output_format: str = "json") -> Dict[str, Any]:
66
+ return {"tool": "design.component_library", "project_path": project_path, "status": "pass-through"}
67
+
68
+
69
+ # ─── Storybook (custom classes, no BaseMCPServer) ─────────────────────
70
+
71
+ def story_generate(component_path: str, story_name: Optional[str] = None, variants: Optional[List[str]] = None) -> Dict[str, Any]:
72
+ return {"tool": "story.generate", "component_path": component_path, "status": "pass-through"}
73
+
74
+
75
+ def story_visual_test(url: str, project_path: Optional[str] = None, threshold: float = 0.05) -> Dict[str, Any]:
76
+ return {"tool": "story.visual_test", "url": url, "status": "pass-through"}
77
+
78
+
79
+ def story_build(project_path: str, output_dir: Optional[str] = None) -> Dict[str, Any]:
80
+ return {"tool": "story.build", "project_path": project_path, "status": "pass-through"}
81
+
82
+
83
+ def story_accessibility_test(project_path: str, standards: str = "WCAG2AA") -> Dict[str, Any]:
84
+ return {"tool": "story.accessibility_test", "project_path": project_path, "status": "pass-through"}
85
+
86
+
87
+ # ─── TestSmith (BaseMCPServer pattern) ─────────────────────────────────
88
+
89
+ def test_generate(project_path: str, source_files: Optional[List[str]] = None, framework: str = "jest") -> Dict[str, Any]:
90
+ return _call("testsmith", "create_testsmith_server", "_tool_generate",
91
+ {"project_path": project_path, "source_files": source_files, "framework": framework}, "test.generate")
92
+
93
+
94
+ def test_coverage(project_path: str, threshold: int = 80) -> Dict[str, Any]:
95
+ return _call("testsmith", "create_testsmith_server", "_tool_coverage",
96
+ {"project_path": project_path, "threshold": threshold}, "test.coverage")
97
+
98
+
99
+ def test_smoke(project_path: str, test_suite: Optional[str] = None) -> Dict[str, Any]:
100
+ result = _call("testsmith", "create_testsmith_server", "_tool_smoke",
101
+ {"project_path": project_path}, "test.smoke")
102
+ # Guard against stub that says "passed" with 0 tests actually run
103
+ if result.get("tests_run", -1) == 0 and result.get("passed") is True:
104
+ return {"tool": "test.smoke", "status": "no_tests",
105
+ "error": "No smoke tests configured. The test runner found 0 tests to execute."}
106
+ return result
107
+
108
+
109
+ # ─── DocsWeaver (BaseMCPServer pattern) ────────────────────────────────
110
+
111
+ def docs_generate(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
112
+ return _call("docsweaver", "create_docsweaver_server", "_tool_generate",
113
+ {"project_path": target, "doc_types": ["api", "readme"], **(options or {})}, "docs.generate")
114
+
115
+
116
+ def docs_validate(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
117
+ return _call("docsweaver", "create_docsweaver_server", "_tool_validate",
118
+ {"docs_path": target, **(options or {})}, "docs.validate")
@@ -0,0 +1,129 @@
1
+ """
2
+ Bridge to delimit-vault package.
3
+ Tier 2 Platform tools — artifact and credential storage.
4
+ """
5
+
6
+ import sys
7
+ import json
8
+ import asyncio
9
+ import logging
10
+ from pathlib import Path
11
+ from typing import Any, Dict, Optional
12
+ from .async_utils import run_async
13
+
14
+ logger = logging.getLogger("delimit.ai.vault_bridge")
15
+
16
+ VAULT_PACKAGE = Path("/home/delimit/.delimit_suite/packages/delimit-vault")
17
+
18
+ _server = None
19
+
20
+
21
+ def _get_server():
22
+ """Return the vault server instance (lazy — no async init here).
23
+
24
+ The Qdrant client uses aiohttp which is bound to the event loop it was
25
+ created on. ``run_async`` creates a *new* loop per call, so we must NOT
26
+ call ``_initialize_clients()`` here. Instead each bridge method calls
27
+ ``_ensure_initialized()`` inside the *same* ``run_async`` invocation that
28
+ performs the actual operation, keeping the aiohttp session alive for the
29
+ duration of the request.
30
+ """
31
+ global _server
32
+ if _server is not None:
33
+ return _server
34
+ pkg_path = str(VAULT_PACKAGE / "delimit_vault_mcp")
35
+ if pkg_path not in sys.path:
36
+ sys.path.insert(0, pkg_path)
37
+ if str(VAULT_PACKAGE) not in sys.path:
38
+ sys.path.insert(0, str(VAULT_PACKAGE))
39
+ try:
40
+ from delimit_vault_mcp.server import DelimitVaultServer
41
+ _server = DelimitVaultServer()
42
+ return _server
43
+ except Exception as e:
44
+ logger.warning(f"Failed to init vault server: {e}")
45
+ return None
46
+
47
+
48
+ async def _ensure_initialized(srv):
49
+ """(Re-)initialize Qdrant client on the *current* event loop.
50
+
51
+ Because ``run_async`` may create a fresh event loop for each bridge call,
52
+ the previous aiohttp session becomes invalid. We unconditionally
53
+ re-initialize to bind the session to the current loop.
54
+ """
55
+ await srv._initialize_clients()
56
+
57
+
58
+ def _extract_text(result) -> Dict[str, Any]:
59
+ """Extract text from MCP TextContent objects or raw results."""
60
+ if isinstance(result, str):
61
+ try:
62
+ return json.loads(result)
63
+ except json.JSONDecodeError:
64
+ return {"result": result}
65
+ if isinstance(result, dict):
66
+ return result
67
+ if isinstance(result, list):
68
+ # Handle list of TextContent objects
69
+ texts = []
70
+ for item in result:
71
+ if hasattr(item, "text"):
72
+ try:
73
+ texts.append(json.loads(item.text))
74
+ except (json.JSONDecodeError, TypeError):
75
+ texts.append({"text": str(item.text)})
76
+ else:
77
+ texts.append(str(item))
78
+ return texts[0] if len(texts) == 1 else {"results": texts}
79
+ if hasattr(result, "text"):
80
+ try:
81
+ return json.loads(result.text)
82
+ except (json.JSONDecodeError, TypeError):
83
+ return {"text": str(result.text)}
84
+ return {"result": str(result)}
85
+
86
+
87
+ def search(query: str) -> Dict[str, Any]:
88
+ """Search vault entries."""
89
+ srv = _get_server()
90
+ if srv is None:
91
+ return {"error": "Vault server unavailable", "results": []}
92
+ try:
93
+ async def _do():
94
+ await _ensure_initialized(srv)
95
+ return await srv._handle_search({"query": query})
96
+ result = run_async(_do())
97
+ return _extract_text(result)
98
+ except Exception as e:
99
+ return {"error": f"Vault search failed: {e}", "results": []}
100
+
101
+
102
+ def health() -> Dict[str, Any]:
103
+ """Check vault health."""
104
+ srv = _get_server()
105
+ if srv is None:
106
+ return {"status": "unavailable", "error": "Vault server not initialized"}
107
+ try:
108
+ async def _do():
109
+ await _ensure_initialized(srv)
110
+ return await srv._handle_health()
111
+ result = run_async(_do())
112
+ return _extract_text(result)
113
+ except Exception as e:
114
+ return {"status": "unavailable", "error": str(e)}
115
+
116
+
117
+ def snapshot(task_id: str = "vault-snapshot") -> Dict[str, Any]:
118
+ """Get vault snapshot."""
119
+ srv = _get_server()
120
+ if srv is None:
121
+ return {"error": "Vault server unavailable"}
122
+ try:
123
+ async def _do():
124
+ await _ensure_initialized(srv)
125
+ return await srv._handle_snapshot({"task_id": task_id})
126
+ result = run_async(_do())
127
+ return _extract_text(result)
128
+ except Exception as e:
129
+ return {"error": f"Vault snapshot failed: {e}"}