cisternal 0.1.0a1__tar.gz

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 (135) hide show
  1. cisternal-0.1.0a1/LICENSE +21 -0
  2. cisternal-0.1.0a1/PKG-INFO +109 -0
  3. cisternal-0.1.0a1/README.md +83 -0
  4. cisternal-0.1.0a1/pyproject.toml +76 -0
  5. cisternal-0.1.0a1/setup.cfg +4 -0
  6. cisternal-0.1.0a1/src/cisternal/__init__.py +150 -0
  7. cisternal-0.1.0a1/src/cisternal/adapters/__init__.py +1 -0
  8. cisternal-0.1.0a1/src/cisternal/adapters/base.py +221 -0
  9. cisternal-0.1.0a1/src/cisternal/adapters/cli.py +85 -0
  10. cisternal-0.1.0a1/src/cisternal/adapters/v2_decorator.py +101 -0
  11. cisternal-0.1.0a1/src/cisternal/adapters/v3_middleware.py +64 -0
  12. cisternal-0.1.0a1/src/cisternal/assets/__init__.py +51 -0
  13. cisternal-0.1.0a1/src/cisternal/assets/bridge.py +140 -0
  14. cisternal-0.1.0a1/src/cisternal/assets/bundle.py +176 -0
  15. cisternal-0.1.0a1/src/cisternal/assets/capability.py +146 -0
  16. cisternal-0.1.0a1/src/cisternal/assets/composite.py +79 -0
  17. cisternal-0.1.0a1/src/cisternal/assets/data/__init__.py +1 -0
  18. cisternal-0.1.0a1/src/cisternal/assets/data/vendor_tools.toml +34 -0
  19. cisternal-0.1.0a1/src/cisternal/assets/inspect_json.py +49 -0
  20. cisternal-0.1.0a1/src/cisternal/assets/load.py +21 -0
  21. cisternal-0.1.0a1/src/cisternal/assets/manifest.py +236 -0
  22. cisternal-0.1.0a1/src/cisternal/assets/manifest_commands.py +48 -0
  23. cisternal-0.1.0a1/src/cisternal/assets/manifest_extensions.py +50 -0
  24. cisternal-0.1.0a1/src/cisternal/assets/protocol.py +15 -0
  25. cisternal-0.1.0a1/src/cisternal/assets/source.py +135 -0
  26. cisternal-0.1.0a1/src/cisternal/assets/spec.py +32 -0
  27. cisternal-0.1.0a1/src/cisternal/assets/validate_golden.py +182 -0
  28. cisternal-0.1.0a1/src/cisternal/cli.py +559 -0
  29. cisternal-0.1.0a1/src/cisternal/export/__init__.py +40 -0
  30. cisternal-0.1.0a1/src/cisternal/export/_hash.py +58 -0
  31. cisternal-0.1.0a1/src/cisternal/export/_markdown.py +33 -0
  32. cisternal-0.1.0a1/src/cisternal/export/_rust_emit.py +116 -0
  33. cisternal-0.1.0a1/src/cisternal/export/antigravity.py +155 -0
  34. cisternal-0.1.0a1/src/cisternal/export/antigravity_rust.py +56 -0
  35. cisternal-0.1.0a1/src/cisternal/export/base.py +65 -0
  36. cisternal-0.1.0a1/src/cisternal/export/claude.py +181 -0
  37. cisternal-0.1.0a1/src/cisternal/export/claude_rust.py +52 -0
  38. cisternal-0.1.0a1/src/cisternal/export/copilot.py +105 -0
  39. cisternal-0.1.0a1/src/cisternal/export/copilot_rust.py +71 -0
  40. cisternal-0.1.0a1/src/cisternal/export/cursor.py +119 -0
  41. cisternal-0.1.0a1/src/cisternal/export/cursor_rust.py +70 -0
  42. cisternal-0.1.0a1/src/cisternal/export/hooks.py +116 -0
  43. cisternal-0.1.0a1/src/cisternal/export/registry.py +94 -0
  44. cisternal-0.1.0a1/src/cisternal/export/sink.py +117 -0
  45. cisternal-0.1.0a1/src/cisternal/export/write.py +59 -0
  46. cisternal-0.1.0a1/src/cisternal/probe/__init__.py +1 -0
  47. cisternal-0.1.0a1/src/cisternal/probe/capability_probe.py +50 -0
  48. cisternal-0.1.0a1/src/cisternal/probe/telemetry_doctor.py +333 -0
  49. cisternal-0.1.0a1/src/cisternal/probe/telemetry_env.py +23 -0
  50. cisternal-0.1.0a1/src/cisternal/py.typed +0 -0
  51. cisternal-0.1.0a1/src/cisternal/registration/__init__.py +46 -0
  52. cisternal-0.1.0a1/src/cisternal/registration/compose.py +89 -0
  53. cisternal-0.1.0a1/src/cisternal/registration/decorator.py +87 -0
  54. cisternal-0.1.0a1/src/cisternal/registration/errors.py +36 -0
  55. cisternal-0.1.0a1/src/cisternal/registration/registry.py +140 -0
  56. cisternal-0.1.0a1/src/cisternal/registration/shim.py +126 -0
  57. cisternal-0.1.0a1/src/cisternal/registration/wired.py +205 -0
  58. cisternal-0.1.0a1/src/cisternal/telemetry/__init__.py +39 -0
  59. cisternal-0.1.0a1/src/cisternal/telemetry/context.py +69 -0
  60. cisternal-0.1.0a1/src/cisternal/telemetry/exporter.py +155 -0
  61. cisternal-0.1.0a1/src/cisternal/telemetry/otlp_exporter.py +255 -0
  62. cisternal-0.1.0a1/src/cisternal/telemetry/pipeline.py +314 -0
  63. cisternal-0.1.0a1/src/cisternal/telemetry/record.py +41 -0
  64. cisternal-0.1.0a1/src/cisternal/telemetry/self_obs.py +245 -0
  65. cisternal-0.1.0a1/src/cisternal/telemetry/span.py +204 -0
  66. cisternal-0.1.0a1/src/cisternal.egg-info/PKG-INFO +109 -0
  67. cisternal-0.1.0a1/src/cisternal.egg-info/SOURCES.txt +133 -0
  68. cisternal-0.1.0a1/src/cisternal.egg-info/dependency_links.txt +1 -0
  69. cisternal-0.1.0a1/src/cisternal.egg-info/entry_points.txt +8 -0
  70. cisternal-0.1.0a1/src/cisternal.egg-info/requires.txt +9 -0
  71. cisternal-0.1.0a1/src/cisternal.egg-info/top_level.txt +1 -0
  72. cisternal-0.1.0a1/tests/test_antigravity_rust_parity.py +74 -0
  73. cisternal-0.1.0a1/tests/test_assets_bundle_m31.py +110 -0
  74. cisternal-0.1.0a1/tests/test_assets_capability.py +49 -0
  75. cisternal-0.1.0a1/tests/test_assets_composite.py +33 -0
  76. cisternal-0.1.0a1/tests/test_assets_manifest.py +172 -0
  77. cisternal-0.1.0a1/tests/test_assets_manifest_extensions.py +101 -0
  78. cisternal-0.1.0a1/tests/test_assets_manifest_vendor_commands.py +75 -0
  79. cisternal-0.1.0a1/tests/test_assets_public_api.py +172 -0
  80. cisternal-0.1.0a1/tests/test_assets_spec.py +294 -0
  81. cisternal-0.1.0a1/tests/test_bathos_telemetry_cutover.py +64 -0
  82. cisternal-0.1.0a1/tests/test_claude_rust_parity.py +100 -0
  83. cisternal-0.1.0a1/tests/test_cli.py +127 -0
  84. cisternal-0.1.0a1/tests/test_cli_assets.py +272 -0
  85. cisternal-0.1.0a1/tests/test_cli_assets_export_surface.py +86 -0
  86. cisternal-0.1.0a1/tests/test_cli_assets_inspect.py +90 -0
  87. cisternal-0.1.0a1/tests/test_cli_assets_validate.py +179 -0
  88. cisternal-0.1.0a1/tests/test_cli_assets_validate_conflict.py +48 -0
  89. cisternal-0.1.0a1/tests/test_cli_native_validate.py +136 -0
  90. cisternal-0.1.0a1/tests/test_cli_rust_parity.py +68 -0
  91. cisternal-0.1.0a1/tests/test_cli_telemetry_doctor.py +397 -0
  92. cisternal-0.1.0a1/tests/test_contemplex_telemetry_cutover.py +59 -0
  93. cisternal-0.1.0a1/tests/test_copilot_rust_parity.py +63 -0
  94. cisternal-0.1.0a1/tests/test_core.py +350 -0
  95. cisternal-0.1.0a1/tests/test_cursor_rust_parity.py +63 -0
  96. cisternal-0.1.0a1/tests/test_dogfood_praxia_fixture.py +59 -0
  97. cisternal-0.1.0a1/tests/test_export_antigravity.py +178 -0
  98. cisternal-0.1.0a1/tests/test_export_claude.py +348 -0
  99. cisternal-0.1.0a1/tests/test_export_claude_bodies.py +135 -0
  100. cisternal-0.1.0a1/tests/test_export_copilot.py +35 -0
  101. cisternal-0.1.0a1/tests/test_export_cursor.py +81 -0
  102. cisternal-0.1.0a1/tests/test_export_hooks_surface.py +92 -0
  103. cisternal-0.1.0a1/tests/test_export_registry.py +58 -0
  104. cisternal-0.1.0a1/tests/test_export_regression_m31b.py +71 -0
  105. cisternal-0.1.0a1/tests/test_export_regression_m31c.py +21 -0
  106. cisternal-0.1.0a1/tests/test_export_regression_m32.py +24 -0
  107. cisternal-0.1.0a1/tests/test_export_sink.py +56 -0
  108. cisternal-0.1.0a1/tests/test_export_write.py +208 -0
  109. cisternal-0.1.0a1/tests/test_golden_matrix.py +72 -0
  110. cisternal-0.1.0a1/tests/test_job_span.py +78 -0
  111. cisternal-0.1.0a1/tests/test_jsonl_exporter.py +116 -0
  112. cisternal-0.1.0a1/tests/test_mcp.py +533 -0
  113. cisternal-0.1.0a1/tests/test_myxcel_adapter.py +37 -0
  114. cisternal-0.1.0a1/tests/test_myxcel_telemetry_cutover.py +67 -0
  115. cisternal-0.1.0a1/tests/test_namefreeze.py +209 -0
  116. cisternal-0.1.0a1/tests/test_otlp_exporter.py +177 -0
  117. cisternal-0.1.0a1/tests/test_otlp_http.py +144 -0
  118. cisternal-0.1.0a1/tests/test_perf.py +136 -0
  119. cisternal-0.1.0a1/tests/test_registration_compose.py +505 -0
  120. cisternal-0.1.0a1/tests/test_registration_init.py +206 -0
  121. cisternal-0.1.0a1/tests/test_registration_registry.py +324 -0
  122. cisternal-0.1.0a1/tests/test_registration_snapshot_public.py +42 -0
  123. cisternal-0.1.0a1/tests/test_registration_telem.py +430 -0
  124. cisternal-0.1.0a1/tests/test_registration_wire.py +525 -0
  125. cisternal-0.1.0a1/tests/test_rust_parity.py +57 -0
  126. cisternal-0.1.0a1/tests/test_rust_parity_bridge.py +30 -0
  127. cisternal-0.1.0a1/tests/test_rust_parity_hook_filter.py +31 -0
  128. cisternal-0.1.0a1/tests/test_rust_parity_matrix.py +60 -0
  129. cisternal-0.1.0a1/tests/test_self_manifest.py +39 -0
  130. cisternal-0.1.0a1/tests/test_selfcheck.py +292 -0
  131. cisternal-0.1.0a1/tests/test_telemetry_import_guard.py +19 -0
  132. cisternal-0.1.0a1/tests/test_validate_golden_resolver.py +37 -0
  133. cisternal-0.1.0a1/tests/test_workflow_export_dogfood.py +57 -0
  134. cisternal-0.1.0a1/tests/test_xperiri_adapter.py +34 -0
  135. cisternal-0.1.0a1/tests/test_xperiri_telemetry_cutover.py +65 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Marielle Russo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: cisternal
3
+ Version: 0.1.0a1
4
+ Summary: Shared telemetry substrate and agent-asset export toolkit for the Praxia tool family
5
+ Author: Marielle Russo
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/maraxen/cisternal
8
+ Project-URL: Repository, https://github.com/maraxen/cisternal
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
+ Requires-Python: >=3.13
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: cyclopts>=4.18.0
18
+ Requires-Dist: fastmcp>=3.4.2
19
+ Requires-Dist: opentelemetry-api>=1.42.1
20
+ Provides-Extra: otlp
21
+ Requires-Dist: opentelemetry-sdk>=1.42.1; extra == "otlp"
22
+ Requires-Dist: opentelemetry-semantic-conventions>=0.63b1; extra == "otlp"
23
+ Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.42.1; extra == "otlp"
24
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.42.1; extra == "otlp"
25
+ Dynamic: license-file
26
+
27
+ # cisternal
28
+
29
+ **Status: alpha.** APIs may change without notice before `1.0`.
30
+
31
+ Cisternal is a shared telemetry substrate and agent-asset export toolkit for the Praxia tool family. It has two parts:
32
+
33
+ - **Telemetry** — a lightweight, non-blocking event pipeline (JSONL export, OTLP export, MCP-tool registration wrapper) for instrumenting Python tools and MCP servers.
34
+ - **Agent-asset export** — a CLI that takes a registry of MCP tools/commands and emits native plugin/config bundles for downstream coding-agent surfaces: Claude Code, Cursor, GitHub Copilot, and Antigravity.
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install cisternal
40
+ ```
41
+
42
+ For OTLP export support:
43
+
44
+ ```bash
45
+ pip install "cisternal[otlp]"
46
+ ```
47
+
48
+ ## Telemetry quickstart
49
+
50
+ ```python
51
+ import cisternal
52
+
53
+ cisternal.init() # log_dir defaults to ~/.cisternal/logs, or env-resolved
54
+
55
+ with cisternal.span("my.operation", request_id="abc123"):
56
+ do_work()
57
+
58
+ cisternal.emit_event("my.custom_event", tool="foo")
59
+ print(cisternal.status())
60
+ ```
61
+
62
+ Check your effective telemetry configuration from the shell:
63
+
64
+ ```bash
65
+ cisternal telemetry doctor
66
+ cisternal telemetry doctor --json --strict
67
+ ```
68
+
69
+ ### Registering MCP tools
70
+
71
+ ```python
72
+ import cisternal
73
+
74
+ @cisternal.tool
75
+ def my_tool(x: int) -> int:
76
+ return x * 2
77
+
78
+ registry = cisternal.wire(server, app, adapter=my_adapter)
79
+ ```
80
+
81
+ `cisternal.tool` is a pure-metadata decorator — it returns the original function unchanged. `cisternal.wire()` snapshots the registry at call time and registers each tool on a FastMCP server (and optionally a Cyclopts CLI app), returning a `WiredRegistry` for introspection.
82
+
83
+ ## Agent-asset export
84
+
85
+ ```bash
86
+ # Preview what would be written, without touching disk
87
+ cisternal assets export --dry-run
88
+
89
+ # Write bundles for specific surfaces
90
+ cisternal assets export --out ./dist/agent-assets
91
+
92
+ # Inspect or validate an existing bundle
93
+ cisternal assets inspect
94
+ cisternal assets validate
95
+ ```
96
+
97
+ Supported export targets: **Claude Code**, **Cursor**, **GitHub Copilot**, **Antigravity**.
98
+
99
+ The CLI is fastmcp-free by design — `cisternal.cli` imports and runs even in environments without `fastmcp` installed; asset-export logic never depends on the telemetry/registration surface.
100
+
101
+ ## Design notes
102
+
103
+ - Telemetry emission never raises: if the pipeline isn't initialized, `emit_event`/`span` are no-ops.
104
+ - Registry state is process-scoped — call `cisternal.clear_registry()` between tests to avoid cross-test contamination.
105
+ - The M2 wire-time MCP callable is a pure passthrough: telemetry and shape adaptation are exclusively owned by the telemetry middleware, never by the registration wrapper itself.
106
+
107
+ ## License
108
+
109
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,83 @@
1
+ # cisternal
2
+
3
+ **Status: alpha.** APIs may change without notice before `1.0`.
4
+
5
+ Cisternal is a shared telemetry substrate and agent-asset export toolkit for the Praxia tool family. It has two parts:
6
+
7
+ - **Telemetry** — a lightweight, non-blocking event pipeline (JSONL export, OTLP export, MCP-tool registration wrapper) for instrumenting Python tools and MCP servers.
8
+ - **Agent-asset export** — a CLI that takes a registry of MCP tools/commands and emits native plugin/config bundles for downstream coding-agent surfaces: Claude Code, Cursor, GitHub Copilot, and Antigravity.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pip install cisternal
14
+ ```
15
+
16
+ For OTLP export support:
17
+
18
+ ```bash
19
+ pip install "cisternal[otlp]"
20
+ ```
21
+
22
+ ## Telemetry quickstart
23
+
24
+ ```python
25
+ import cisternal
26
+
27
+ cisternal.init() # log_dir defaults to ~/.cisternal/logs, or env-resolved
28
+
29
+ with cisternal.span("my.operation", request_id="abc123"):
30
+ do_work()
31
+
32
+ cisternal.emit_event("my.custom_event", tool="foo")
33
+ print(cisternal.status())
34
+ ```
35
+
36
+ Check your effective telemetry configuration from the shell:
37
+
38
+ ```bash
39
+ cisternal telemetry doctor
40
+ cisternal telemetry doctor --json --strict
41
+ ```
42
+
43
+ ### Registering MCP tools
44
+
45
+ ```python
46
+ import cisternal
47
+
48
+ @cisternal.tool
49
+ def my_tool(x: int) -> int:
50
+ return x * 2
51
+
52
+ registry = cisternal.wire(server, app, adapter=my_adapter)
53
+ ```
54
+
55
+ `cisternal.tool` is a pure-metadata decorator — it returns the original function unchanged. `cisternal.wire()` snapshots the registry at call time and registers each tool on a FastMCP server (and optionally a Cyclopts CLI app), returning a `WiredRegistry` for introspection.
56
+
57
+ ## Agent-asset export
58
+
59
+ ```bash
60
+ # Preview what would be written, without touching disk
61
+ cisternal assets export --dry-run
62
+
63
+ # Write bundles for specific surfaces
64
+ cisternal assets export --out ./dist/agent-assets
65
+
66
+ # Inspect or validate an existing bundle
67
+ cisternal assets inspect
68
+ cisternal assets validate
69
+ ```
70
+
71
+ Supported export targets: **Claude Code**, **Cursor**, **GitHub Copilot**, **Antigravity**.
72
+
73
+ The CLI is fastmcp-free by design — `cisternal.cli` imports and runs even in environments without `fastmcp` installed; asset-export logic never depends on the telemetry/registration surface.
74
+
75
+ ## Design notes
76
+
77
+ - Telemetry emission never raises: if the pipeline isn't initialized, `emit_event`/`span` are no-ops.
78
+ - Registry state is process-scoped — call `cisternal.clear_registry()` between tests to avoid cross-test contamination.
79
+ - The M2 wire-time MCP callable is a pure passthrough: telemetry and shape adaptation are exclusively owned by the telemetry middleware, never by the registration wrapper itself.
80
+
81
+ ## License
82
+
83
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,76 @@
1
+ [build-system]
2
+ requires = ["setuptools", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "cisternal"
7
+ version = "0.1.0a1"
8
+ description = "Shared telemetry substrate and agent-asset export toolkit for the Praxia tool family"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ authors = [
12
+ { name = "Marielle Russo" },
13
+ ]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.13",
18
+ "Operating System :: OS Independent",
19
+ "Topic :: Software Development :: Libraries :: Python Modules",
20
+ ]
21
+ requires-python = ">=3.13"
22
+ dependencies = [
23
+ "cyclopts>=4.18.0",
24
+ "fastmcp>=3.4.2",
25
+ "opentelemetry-api>=1.42.1",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://github.com/maraxen/cisternal"
30
+ Repository = "https://github.com/maraxen/cisternal"
31
+
32
+ [project.optional-dependencies]
33
+ otlp = [
34
+ "opentelemetry-sdk>=1.42.1",
35
+ "opentelemetry-semantic-conventions>=0.63b1",
36
+ "opentelemetry-exporter-otlp-proto-grpc>=1.42.1",
37
+ "opentelemetry-exporter-otlp-proto-http>=1.42.1",
38
+ ]
39
+
40
+ [dependency-groups]
41
+ dev = [
42
+ "pytest>=9.0.3",
43
+ "pytest-asyncio>=0.24.0",
44
+ "pytest-cov>=7.1.0",
45
+ "ruff>=0.15.17",
46
+ "ty>=0.0.49",
47
+ "opentelemetry-sdk>=1.42.1",
48
+ "opentelemetry-semantic-conventions>=0.63b1",
49
+ "opentelemetry-exporter-otlp-proto-grpc>=1.42.1",
50
+ "opentelemetry-exporter-otlp-proto-http>=1.42.1",
51
+ ]
52
+
53
+ [project.scripts]
54
+ cisternal = "cisternal.cli:app"
55
+
56
+ [project.entry-points."cisternal.emitters"]
57
+ claude = "cisternal.export.registry:claude_factory"
58
+ cursor = "cisternal.export.registry:cursor_factory"
59
+ copilot = "cisternal.export.registry:copilot_factory"
60
+ antigravity = "cisternal.export.registry:antigravity_factory"
61
+
62
+ [tool.setuptools]
63
+ package-dir = { "" = "src" }
64
+
65
+ [tool.setuptools.packages.find]
66
+ where = ["src"]
67
+
68
+ [tool.setuptools.package-data]
69
+ "cisternal.assets" = ["data/*.toml"]
70
+
71
+ [tool.pytest.ini_options]
72
+ testpaths = ["tests"]
73
+ markers = [
74
+ "integration: requires external services (e.g. otel-collector on localhost)",
75
+ "golden_matrix: export trust digest matrix (M11)",
76
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,150 @@
1
+ """Cisternal: Shared telemetry substrate for praxia tool family.
2
+
3
+ Public API (spec §3.2 — M1 telemetry + M2 registration surface):
4
+
5
+ M1 — Telemetry:
6
+ init(log_dir, max_bytes, backup_count, exporters): Initialize pipeline
7
+ emit_event(name, **fields): Emit a telemetry event
8
+ span(name, **fields): Sync timing context manager
9
+ aspan(name, **fields): Async timing context manager
10
+ status(): Get pipeline health status
11
+
12
+ M2 — Registration surface (B+G2 hybrid design, challenger-hardened):
13
+ tool: Pure-metadata decorator for registering MCP tools (A1, A2).
14
+ @cisternal.tool returns the original fn unchanged (decorated_fn is fn).
15
+ wire(server, app, *, adapter, registry, expected, validate):
16
+ Snapshot a registry at call time and register each tool on a FastMCP
17
+ server (and optionally a Cyclopts App). Returns a WiredRegistry.
18
+ WiredRegistry: Introspection object returned by wire().
19
+ CisternalWireError: Raised by wire() when expected tools are missing.
20
+ clear_registry(name): Test teardown helper; clears a named registry (A7).
21
+
22
+ Design assumptions (spec §assumptions):
23
+ A1 — FastMCP v3 uses asyncio.iscoroutinefunction() to decide whether to await
24
+ the tool callable. The generated MCP callable is always async def.
25
+ A2 — FastMCP v3 reads inspect.signature(fn) for JSON schema generation.
26
+ Explicit __signature__ injection (H1) controls what FastMCP sees.
27
+ A3 — M1 CisternalMiddleware is installed on the FastMCP server before wire()
28
+ is called. Telemetry and shape adaptation are M1's exclusive responsibility.
29
+ A4 — Cyclopts 4.18.0+ calls asyncio.run() for async def command functions when
30
+ no event loop is running; inside a running loop, use app.run_async().
31
+ A5 — Global registry state is process-scoped; no cross-process sharing.
32
+ A6 — Python >= 3.11 (asyncio.get_running_loop() is the stable API).
33
+ A7 — Test environments call cisternal.clear_registry() in teardown to prevent
34
+ cross-test registry contamination.
35
+
36
+ HARD INVARIANT (C5/AC-M2-6):
37
+ The M2 wire-time MCP callable is a PURE PASSTHROUGH — it MUST NOT call any
38
+ adapter.emit_* or adapter.shape_* methods, or emit ANY telemetry. All
39
+ telemetry and shaping is exclusively owned by M1 CisternalMiddleware.
40
+ """
41
+
42
+ from pathlib import Path
43
+ from typing import Any
44
+
45
+ from cisternal.telemetry import (
46
+ init_pipeline,
47
+ get_pipeline,
48
+ span,
49
+ aspan,
50
+ job_span,
51
+ status,
52
+ ExporterBase,
53
+ _build_record,
54
+ )
55
+ from cisternal.registration.decorator import tool
56
+ from cisternal.registration.errors import CisternalWireError
57
+ from cisternal.registration.registry import clear_registry
58
+
59
+ # M3 (assets export)
60
+ from cisternal.assets.spec import AssetSpec
61
+ from cisternal.assets.bundle import AssetBundle
62
+ from cisternal.assets.source import registry_assets
63
+ from cisternal.export.base import Emitter
64
+ from cisternal.export.claude import ClaudeEmitter
65
+ from cisternal.export.write import write_bundle
66
+
67
+
68
+ def init(
69
+ log_dir: str | Path | None = None,
70
+ max_bytes: int = 10_485_760,
71
+ backup_count: int = 5,
72
+ exporters: list[ExporterBase] | None = None,
73
+ heartbeat_interval: float = 30.0,
74
+ ) -> None:
75
+ """Initialize the telemetry pipeline (idempotent).
76
+
77
+ Args:
78
+ log_dir: Directory for JSONL logs. If None, resolves via env vars or defaults to ~/.cisternal/logs.
79
+ max_bytes: Max file size before rotation (default 10 MB).
80
+ backup_count: Number of backup files to keep.
81
+ exporters: Custom exporters. If None, uses JsonlExporter with log_dir.
82
+ heartbeat_interval: Seconds between liveness heartbeat probes (default 30s).
83
+ """
84
+ init_pipeline(
85
+ log_dir=log_dir,
86
+ max_bytes=max_bytes,
87
+ backup_count=backup_count,
88
+ exporters=exporters,
89
+ heartbeat_interval=heartbeat_interval,
90
+ )
91
+
92
+
93
+ def emit_event(name: str, **fields: Any) -> None:
94
+ """Emit a telemetry event.
95
+
96
+ Snapshots contextvars on this thread, builds a Record, and enqueues
97
+ it for non-blocking export. Never raises.
98
+
99
+ Args:
100
+ name: Event name (e.g. 'mcp.call_start').
101
+ **fields: Event fields (e.g. tool='foo', request_id='xyz').
102
+ """
103
+ import time
104
+
105
+ pipeline = get_pipeline()
106
+ if pipeline is None:
107
+ return
108
+
109
+ record = _build_record(name, ts=time.time(), **fields)
110
+ if record is not None:
111
+ pipeline.emit(record)
112
+
113
+
114
+ def _lazy_import(name: str) -> object:
115
+ """Lazy re-export for wire and WiredRegistry to defer fastmcp import."""
116
+ if name == "wire":
117
+ from cisternal.registration.wired import wire as _wire
118
+ return _wire
119
+ if name == "WiredRegistry":
120
+ from cisternal.registration.wired import WiredRegistry as _WiredRegistry
121
+ return _WiredRegistry
122
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
123
+
124
+
125
+ def __getattr__(name: str) -> object:
126
+ return _lazy_import(name)
127
+
128
+
129
+ __all__ = [
130
+ # M1 — Telemetry
131
+ "init",
132
+ "emit_event",
133
+ "span",
134
+ "aspan",
135
+ "job_span",
136
+ "status",
137
+ # M2 — Registration surface
138
+ "tool",
139
+ "wire",
140
+ "WiredRegistry",
141
+ "CisternalWireError",
142
+ "clear_registry",
143
+ # M3 (assets export)
144
+ "AssetSpec",
145
+ "AssetBundle",
146
+ "registry_assets",
147
+ "Emitter",
148
+ "ClaudeEmitter",
149
+ "write_bundle",
150
+ ]
@@ -0,0 +1 @@
1
+ """Consumer adapters: FastMCP v3 middleware, v2 decorator, CLI."""
@@ -0,0 +1,221 @@
1
+ """AdapterBase: Consumer-agnostic adapter protocol (spec §3.6, §5.4).
2
+
3
+ Each adapter owns the event-name set (ALLOWED_NAMES) and the response shape
4
+ for its consumer (shape_ok/shape_error). The MCP wrappers (CisternalMiddleware,
5
+ traced_tool) instantiate an adapter, delegate telemetry emission + shaping to it,
6
+ and never re-raise exceptions (CH-5).
7
+
8
+ (CH-9) Runtime name guard: emit_start/end/error check that the emitted name is
9
+ in self.ALLOWED_NAMES; on mismatch, calls _swallow_name_error (stderr warn + continue).
10
+ Tests monkeypatch _swallow_name_error to raise AssertionError instead.
11
+ """
12
+
13
+ from abc import ABC, abstractmethod
14
+ import json
15
+ import sys
16
+ from typing import Any
17
+
18
+ from cisternal import emit_event
19
+
20
+
21
+ class AdapterBase(ABC):
22
+ """Abstract base for MCP tool adapters (v3 middleware, v2 decorator, etc.).
23
+
24
+ Each subclass defines:
25
+ - ALLOWED_NAMES: frozenset[str] of event names this adapter may emit.
26
+ - shape_ok(tool_name, result): Transform success response.
27
+ - shape_error(tool_name, exc): Transform error response.
28
+ """
29
+
30
+ ALLOWED_NAMES: frozenset[str]
31
+
32
+ def emit_start(self, tool_name: str, arg_keys: list[str], request_id: str) -> None:
33
+ """Emit mcp.call_start event.
34
+
35
+ Args:
36
+ tool_name: Name of the tool being called.
37
+ arg_keys: Sorted list of argument keys (spec §3.7, §3.8).
38
+ request_id: Unique request ID for this invocation.
39
+ """
40
+ name = "mcp.call_start"
41
+ if name not in self.ALLOWED_NAMES:
42
+ self._swallow_name_error(name)
43
+ emit_event(name, tool=tool_name, arg_keys=arg_keys, request_id=request_id)
44
+
45
+ def emit_end(self, tool_name: str, request_id: str, duration_ms: float) -> None:
46
+ """Emit mcp.call_end event.
47
+
48
+ Args:
49
+ tool_name: Name of the tool that executed.
50
+ request_id: The request ID from emit_start.
51
+ duration_ms: Duration of execution in milliseconds.
52
+ """
53
+ name = "mcp.call_end"
54
+ if name not in self.ALLOWED_NAMES:
55
+ self._swallow_name_error(name)
56
+ emit_event(name, tool=tool_name, request_id=request_id, duration_ms=duration_ms)
57
+
58
+ def emit_error(self, tool_name: str, request_id: str, exc: BaseException) -> None:
59
+ """Emit mcp.tool_error event.
60
+
61
+ Args:
62
+ tool_name: Name of the tool that raised.
63
+ request_id: The request ID from emit_start.
64
+ exc: The exception that was raised.
65
+ """
66
+ name = "mcp.tool_error"
67
+ if name not in self.ALLOWED_NAMES:
68
+ self._swallow_name_error(name)
69
+ emit_event(
70
+ name,
71
+ tool=tool_name,
72
+ request_id=request_id,
73
+ exc_type=type(exc).__name__,
74
+ exc_msg=str(exc),
75
+ )
76
+
77
+ @abstractmethod
78
+ def shape_ok(self, tool_name: str, result: Any) -> Any:
79
+ """Shape a successful tool result for the consumer.
80
+
81
+ Args:
82
+ tool_name: Name of the tool (for context).
83
+ result: The result returned by the tool.
84
+
85
+ Returns:
86
+ Consumer-specific shaped response (dict, str, or other).
87
+ """
88
+ pass
89
+
90
+ @abstractmethod
91
+ def shape_error(self, tool_name: str, exc: BaseException, **fields: Any) -> Any:
92
+ """Shape an error response for the consumer (never re-raises).
93
+
94
+ Args:
95
+ tool_name: Name of the tool (for context).
96
+ exc: The exception that was caught.
97
+ **fields: Optional extra fields for the error envelope.
98
+
99
+ Returns:
100
+ Consumer-specific error shape (dict, str, or other).
101
+ """
102
+ pass
103
+
104
+ def _swallow_name_error(self, name: str) -> None:
105
+ """Handle illegal event name: log to stderr and return (warn-and-continue).
106
+
107
+ Tests may monkeypatch this to raise AssertionError (AC-NAMEFREEZE-4).
108
+
109
+ Args:
110
+ name: The illegal event name.
111
+ """
112
+ print(f"[cisternal] ILLEGAL event name: {name!r}", file=sys.stderr)
113
+
114
+
115
+ class BathosAdapter(AdapterBase):
116
+ """Adapter for bathos v3 middleware.
117
+
118
+ Event names (spec §4.2): mcp.call_start, mcp.call_end, mcp.tool_error.
119
+ Response shape: dict envelope with ok/error_code/error/resolution_hint.
120
+ """
121
+
122
+ ALLOWED_NAMES = frozenset({"mcp.call_start", "mcp.call_end", "mcp.tool_error"})
123
+
124
+ def shape_ok(self, tool_name: str, result: Any) -> Any:
125
+ """Shape success: merge dict results, add envelope fields.
126
+
127
+ If result is a dict, spread it and add envelope.
128
+ Otherwise, return minimal envelope.
129
+ """
130
+ if isinstance(result, dict):
131
+ return {
132
+ **result,
133
+ "ok": True,
134
+ "error_code": None,
135
+ "error": None,
136
+ "resolution_hint": None,
137
+ }
138
+ return {
139
+ "ok": True,
140
+ "error_code": None,
141
+ "error": None,
142
+ "resolution_hint": None,
143
+ }
144
+
145
+ def shape_error(self, tool_name: str, exc: BaseException, **fields: Any) -> Any:
146
+ """Shape error: error envelope with error_code and message."""
147
+ return {
148
+ "ok": False,
149
+ "error_code": "INTERNAL",
150
+ "error": str(exc),
151
+ "resolution_hint": "",
152
+ }
153
+
154
+
155
+ class ContemplexAdapter(AdapterBase):
156
+ """Adapter for contemplex v2 decorator (sync).
157
+
158
+ Event names (spec §4.2): mcp.call_start, mcp.call_end, mcp.tool_error.
159
+ Response shape: passthrough for success; err_envelope for error.
160
+ """
161
+
162
+ ALLOWED_NAMES = frozenset({"mcp.call_start", "mcp.call_end", "mcp.tool_error"})
163
+
164
+ def shape_ok(self, tool_name: str, result: Any) -> Any:
165
+ """Shape success: passthrough result unchanged."""
166
+ return result
167
+
168
+ def shape_error(self, tool_name: str, exc: BaseException, **fields: Any) -> Any:
169
+ """Shape error: use contemplex err_envelope if available, else fallback.
170
+
171
+ Tries to import contemplex.errors; if unavailable, returns basic dict.
172
+ """
173
+ try:
174
+ from contemplex.errors import ErrorCode, err_envelope
175
+
176
+ return err_envelope(ErrorCode.INTERNAL, f"{type(exc).__name__}: {exc}")
177
+ except ImportError:
178
+ # Fallback if contemplex is not available
179
+ return {"ok": False, "error_code": "INTERNAL", "error": str(exc)}
180
+
181
+
182
+ class XpeririAdapter(AdapterBase):
183
+ """Adapter for xperiri v2 decorator (sync, JSON-string MCP returns).
184
+
185
+ Event names (spec §4.2): mcp.call_start, mcp.call_end, mcp.tool_error.
186
+ Response shape: JSON string for success and error (xperiri MCP tools return str).
187
+ """
188
+
189
+ ALLOWED_NAMES = frozenset({"mcp.call_start", "mcp.call_end", "mcp.tool_error"})
190
+
191
+ def shape_ok(self, tool_name: str, result: Any) -> Any:
192
+ """Shape success: passthrough str; serialize dict/other to JSON."""
193
+ if isinstance(result, str):
194
+ return result
195
+ return json.dumps(result, sort_keys=True)
196
+
197
+ def shape_error(self, tool_name: str, exc: BaseException, **fields: Any) -> Any:
198
+ """Shape error: JSON string envelope matching xperiri error returns."""
199
+ payload: dict[str, Any] = {"ok": False, "error": str(exc)}
200
+ payload.update(fields)
201
+ return json.dumps(payload, sort_keys=True)
202
+
203
+
204
+ class MyxcelAdapter(AdapterBase):
205
+ """Adapter for myxcel v2 decorator (async MCP tools, dict returns).
206
+
207
+ Event names (spec §4.2): mcp.call_start, mcp.call_end, mcp.tool_error.
208
+ Response shape: dict passthrough; errors use {error, message} per myxcel.mcp_server._tool_error.
209
+ """
210
+
211
+ ALLOWED_NAMES = frozenset({"mcp.call_start", "mcp.call_end", "mcp.tool_error"})
212
+
213
+ def shape_ok(self, tool_name: str, result: Any) -> Any:
214
+ """Shape success: passthrough dict/list results (myxcel in-band errors included)."""
215
+ if isinstance(result, (dict, list)):
216
+ return result
217
+ return {"result": result}
218
+
219
+ def shape_error(self, tool_name: str, exc: BaseException, **fields: Any) -> Any:
220
+ """Shape error: myxcel _tool_error envelope."""
221
+ return {"error": type(exc).__name__, "message": str(exc)}