nthlayer-workers 1.0.0__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 (180) hide show
  1. nthlayer_workers-1.0.0/PKG-INFO +19 -0
  2. nthlayer_workers-1.0.0/pyproject.toml +41 -0
  3. nthlayer_workers-1.0.0/setup.cfg +4 -0
  4. nthlayer_workers-1.0.0/src/nthlayer_workers/__init__.py +5 -0
  5. nthlayer_workers-1.0.0/src/nthlayer_workers/cli.py +234 -0
  6. nthlayer_workers-1.0.0/src/nthlayer_workers/correlate/__init__.py +1 -0
  7. nthlayer_workers-1.0.0/src/nthlayer_workers/correlate/cli.py +847 -0
  8. nthlayer_workers-1.0.0/src/nthlayer_workers/correlate/config.py +111 -0
  9. nthlayer_workers-1.0.0/src/nthlayer_workers/correlate/correlation/__init__.py +1 -0
  10. nthlayer_workers-1.0.0/src/nthlayer_workers/correlate/correlation/changes.py +87 -0
  11. nthlayer_workers-1.0.0/src/nthlayer_workers/correlate/correlation/dedup.py +62 -0
  12. nthlayer_workers-1.0.0/src/nthlayer_workers/correlate/correlation/engine.py +244 -0
  13. nthlayer_workers-1.0.0/src/nthlayer_workers/correlate/correlation/temporal.py +79 -0
  14. nthlayer_workers-1.0.0/src/nthlayer_workers/correlate/correlation/topology.py +104 -0
  15. nthlayer_workers-1.0.0/src/nthlayer_workers/correlate/ingestion/__init__.py +1 -0
  16. nthlayer_workers-1.0.0/src/nthlayer_workers/correlate/ingestion/protocol.py +10 -0
  17. nthlayer_workers-1.0.0/src/nthlayer_workers/correlate/ingestion/severity.py +18 -0
  18. nthlayer_workers-1.0.0/src/nthlayer_workers/correlate/ingestion/webhook.py +197 -0
  19. nthlayer_workers-1.0.0/src/nthlayer_workers/correlate/notifications.py +85 -0
  20. nthlayer_workers-1.0.0/src/nthlayer_workers/correlate/prometheus.py +234 -0
  21. nthlayer_workers-1.0.0/src/nthlayer_workers/correlate/reasoning.py +375 -0
  22. nthlayer_workers-1.0.0/src/nthlayer_workers/correlate/session.py +189 -0
  23. nthlayer_workers-1.0.0/src/nthlayer_workers/correlate/snapshot/__init__.py +1 -0
  24. nthlayer_workers-1.0.0/src/nthlayer_workers/correlate/snapshot/generator.py +170 -0
  25. nthlayer_workers-1.0.0/src/nthlayer_workers/correlate/snapshot/model.py +177 -0
  26. nthlayer_workers-1.0.0/src/nthlayer_workers/correlate/snapshot/token.py +14 -0
  27. nthlayer_workers-1.0.0/src/nthlayer_workers/correlate/state.py +88 -0
  28. nthlayer_workers-1.0.0/src/nthlayer_workers/correlate/store/__init__.py +5 -0
  29. nthlayer_workers-1.0.0/src/nthlayer_workers/correlate/store/protocol.py +48 -0
  30. nthlayer_workers-1.0.0/src/nthlayer_workers/correlate/store/sqlite.py +443 -0
  31. nthlayer_workers-1.0.0/src/nthlayer_workers/correlate/summary.py +180 -0
  32. nthlayer_workers-1.0.0/src/nthlayer_workers/correlate/traces/__init__.py +1 -0
  33. nthlayer_workers-1.0.0/src/nthlayer_workers/correlate/traces/protocol.py +120 -0
  34. nthlayer_workers-1.0.0/src/nthlayer_workers/correlate/traces/tempo.py +667 -0
  35. nthlayer_workers-1.0.0/src/nthlayer_workers/correlate/traces/topology.py +39 -0
  36. nthlayer_workers-1.0.0/src/nthlayer_workers/correlate/types.py +77 -0
  37. nthlayer_workers-1.0.0/src/nthlayer_workers/correlate/worker.py +630 -0
  38. nthlayer_workers-1.0.0/src/nthlayer_workers/learn/__init__.py +5 -0
  39. nthlayer_workers-1.0.0/src/nthlayer_workers/learn/__main__.py +5 -0
  40. nthlayer_workers-1.0.0/src/nthlayer_workers/learn/cli.py +164 -0
  41. nthlayer_workers-1.0.0/src/nthlayer_workers/learn/retrospective.py +381 -0
  42. nthlayer_workers-1.0.0/src/nthlayer_workers/learn/trends.py +102 -0
  43. nthlayer_workers-1.0.0/src/nthlayer_workers/learn/worker.py +366 -0
  44. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/__init__.py +3 -0
  45. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/__main__.py +5 -0
  46. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/_parsing.py +15 -0
  47. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/adapters/__init__.py +0 -0
  48. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/adapters/_util.py +24 -0
  49. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/adapters/devin.py +119 -0
  50. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/adapters/gastown.py +88 -0
  51. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/adapters/prometheus.py +277 -0
  52. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/adapters/protocol.py +20 -0
  53. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/adapters/webhook.py +161 -0
  54. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/api/__init__.py +0 -0
  55. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/api/normalise.py +50 -0
  56. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/api/queue.py +243 -0
  57. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/api/response.py +51 -0
  58. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/api/server.py +504 -0
  59. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/calibration/__init__.py +0 -0
  60. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/calibration/loop.py +62 -0
  61. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/calibration/slos.py +212 -0
  62. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/calibration/verdict_calibration.py +31 -0
  63. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/cli.py +753 -0
  64. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/config.py +191 -0
  65. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/detection/__init__.py +6 -0
  66. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/detection/detector.py +82 -0
  67. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/detection/protocol.py +29 -0
  68. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/governance/__init__.py +0 -0
  69. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/governance/engine.py +163 -0
  70. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/manifest.py +77 -0
  71. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/notifications.py +53 -0
  72. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/pipeline/__init__.py +0 -0
  73. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/pipeline/evaluator.py +155 -0
  74. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/pipeline/router.py +160 -0
  75. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/store/__init__.py +0 -0
  76. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/store/protocol.py +38 -0
  77. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/store/sqlite.py +276 -0
  78. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/telemetry.py +116 -0
  79. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/tiering/__init__.py +0 -0
  80. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/tiering/classifier.py +58 -0
  81. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/tiering/promotion.py +118 -0
  82. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/trends/__init__.py +0 -0
  83. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/trends/tracker.py +72 -0
  84. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/types.py +75 -0
  85. nthlayer_workers-1.0.0/src/nthlayer_workers/measure/worker.py +439 -0
  86. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/__init__.py +25 -0
  87. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/__main__.py +5 -0
  88. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/api/__init__.py +1 -0
  89. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/assessment.py +95 -0
  90. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/cli.py +737 -0
  91. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/config.py +11 -0
  92. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/db/__init__.py +1 -0
  93. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/decision_records.py +220 -0
  94. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/dependencies/__init__.py +18 -0
  95. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/dependencies/discovery.py +294 -0
  96. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/dependencies/providers/__init__.py +48 -0
  97. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/dependencies/providers/backstage.py +467 -0
  98. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/dependencies/providers/base.py +76 -0
  99. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/dependencies/providers/consul.py +518 -0
  100. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/dependencies/providers/etcd.py +360 -0
  101. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/dependencies/providers/kubernetes.py +682 -0
  102. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/dependencies/providers/prometheus.py +368 -0
  103. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/dependencies/providers/zookeeper.py +399 -0
  104. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/deployments/__init__.py +1 -0
  105. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/discovery/__init__.py +14 -0
  106. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/discovery/classifier.py +66 -0
  107. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/discovery/client.py +189 -0
  108. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/discovery/models.py +53 -0
  109. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/drift/__init__.py +26 -0
  110. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/drift/analyzer.py +383 -0
  111. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/drift/models.py +174 -0
  112. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/drift/patterns.py +88 -0
  113. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/explanation.py +118 -0
  114. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/gate/__init__.py +39 -0
  115. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/gate/conditions.py +92 -0
  116. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/gate/correlator.py +154 -0
  117. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/gate/evaluator.py +192 -0
  118. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/gate/policies.py +226 -0
  119. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/gate_adapter.py +40 -0
  120. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/incident.py +36 -0
  121. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/portfolio/__init__.py +17 -0
  122. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/portfolio/aggregator.py +168 -0
  123. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/portfolio/scorer.py +13 -0
  124. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/slo/__init__.py +19 -0
  125. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/slo/collector.py +235 -0
  126. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/slo/spec_loader.py +40 -0
  127. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/sqlite_store.py +152 -0
  128. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/store.py +92 -0
  129. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/verification/__init__.py +22 -0
  130. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/verification/exporter_guidance.py +146 -0
  131. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/verification/extractor.py +127 -0
  132. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/verification/models.py +101 -0
  133. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/verification/verifier.py +111 -0
  134. nthlayer_workers-1.0.0/src/nthlayer_workers/observe/worker.py +332 -0
  135. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/__init__.py +2 -0
  136. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/__main__.py +4 -0
  137. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/agents/__init__.py +0 -0
  138. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/agents/base.py +556 -0
  139. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/agents/communication.py +115 -0
  140. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/agents/investigation.py +124 -0
  141. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/agents/remediation.py +219 -0
  142. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/agents/triage.py +132 -0
  143. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/cli.py +772 -0
  144. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/config.py +135 -0
  145. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/context_store.py +256 -0
  146. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/coordinator.py +487 -0
  147. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/metrics.py +104 -0
  148. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/notification_backends/__init__.py +1 -0
  149. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/notification_backends/ntfy_backend.py +158 -0
  150. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/notification_backends/protocol.py +59 -0
  151. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/notification_backends/slack_backend.py +203 -0
  152. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/notification_backends/stdout_backend.py +56 -0
  153. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/notifications.py +247 -0
  154. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/oncall/__init__.py +1 -0
  155. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/oncall/escalation.py +103 -0
  156. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/oncall/runner.py +193 -0
  157. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/oncall/schedule.py +243 -0
  158. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/safe_actions/__init__.py +0 -0
  159. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/safe_actions/actions.py +139 -0
  160. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/safe_actions/registry.py +171 -0
  161. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/safe_actions/webhook.py +194 -0
  162. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/server.py +357 -0
  163. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/sre/__init__.py +1 -0
  164. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/sre/brief.py +175 -0
  165. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/sre/delegation.py +101 -0
  166. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/sre/post_incident.py +146 -0
  167. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/sre/shift_report.py +129 -0
  168. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/sre/suppression.py +91 -0
  169. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/types.py +109 -0
  170. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/verdict_submission.py +56 -0
  171. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/worker.py +533 -0
  172. nthlayer_workers-1.0.0/src/nthlayer_workers/respond/worker_helpers.py +140 -0
  173. nthlayer_workers-1.0.0/src/nthlayer_workers/runner.py +198 -0
  174. nthlayer_workers-1.0.0/src/nthlayer_workers.egg-info/PKG-INFO +19 -0
  175. nthlayer_workers-1.0.0/src/nthlayer_workers.egg-info/SOURCES.txt +178 -0
  176. nthlayer_workers-1.0.0/src/nthlayer_workers.egg-info/dependency_links.txt +1 -0
  177. nthlayer_workers-1.0.0/src/nthlayer_workers.egg-info/entry_points.txt +2 -0
  178. nthlayer_workers-1.0.0/src/nthlayer_workers.egg-info/requires.txt +14 -0
  179. nthlayer_workers-1.0.0/src/nthlayer_workers.egg-info/top_level.txt +1 -0
  180. nthlayer_workers-1.0.0/tests/test_runner.py +238 -0
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.4
2
+ Name: nthlayer-workers
3
+ Version: 1.0.0
4
+ Summary: NthLayer workers — Tier 2 background computation (observe, measure, correlate, respond, learn)
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: nthlayer-common>=0.1.8
8
+ Requires-Dist: httpx>=0.27
9
+ Requires-Dist: pyyaml>=6.0
10
+ Requires-Dist: structlog>=24.1.0
11
+ Requires-Dist: scipy>=1.11
12
+ Requires-Dist: numpy>=1.24
13
+ Requires-Dist: starlette>=0.40
14
+ Requires-Dist: uvicorn>=0.30
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest>=8.2; extra == "dev"
17
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
18
+ Requires-Dist: respx>=0.21; extra == "dev"
19
+ Requires-Dist: ruff>=0.8; extra == "dev"
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "nthlayer-workers"
7
+ version = "1.0.0"
8
+ description = "NthLayer workers — Tier 2 background computation (observe, measure, correlate, respond, learn)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ dependencies = [
12
+ "nthlayer-common>=0.1.8",
13
+ "httpx>=0.27",
14
+ "pyyaml>=6.0",
15
+ "structlog>=24.1.0",
16
+ "scipy>=1.11",
17
+ "numpy>=1.24",
18
+ "starlette>=0.40",
19
+ "uvicorn>=0.30",
20
+ ]
21
+
22
+ [project.scripts]
23
+ nthlayer-workers = "nthlayer_workers.cli:main"
24
+
25
+ [project.optional-dependencies]
26
+ dev = [
27
+ "pytest>=8.2",
28
+ "pytest-asyncio>=0.23",
29
+ "respx>=0.21",
30
+ "ruff>=0.8",
31
+ ]
32
+
33
+ [tool.uv.sources]
34
+ nthlayer-common = { path = "../nthlayer-common", editable = true }
35
+
36
+ [tool.setuptools.packages.find]
37
+ where = ["src"]
38
+
39
+ [tool.pytest.ini_options]
40
+ asyncio_mode = "auto"
41
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """NthLayer workers — Tier 2 background computation modules.
2
+
3
+ Internal modules: observe, measure, correlate, respond, learn.
4
+ Communicates with nthlayer-core exclusively via HTTP API.
5
+ """
@@ -0,0 +1,234 @@
1
+ """NthLayer workers CLI — run all worker modules."""
2
+
3
+ import argparse
4
+ import asyncio
5
+ import sys
6
+
7
+ from nthlayer_workers.runner import ModuleRunner
8
+
9
+
10
+ def main():
11
+ parser = argparse.ArgumentParser(description="NthLayer workers")
12
+ parser.add_argument("-V", "--version", action="version", version="%(prog)s 1.5.0a1")
13
+ sub = parser.add_subparsers(dest="command")
14
+
15
+ # -- serve: start all worker modules --
16
+ serve_parser = sub.add_parser("serve", help="Start the workers process (all modules)")
17
+ serve_parser.add_argument("--core-url", default="http://localhost:8000",
18
+ help="Core API URL (default: http://localhost:8000)")
19
+ serve_parser.add_argument("--instance-id", default="workers-1",
20
+ help="Instance ID for heartbeats (default: workers-1)")
21
+ serve_parser.add_argument("--prometheus-url", default="http://localhost:9090",
22
+ help="Prometheus URL (default: http://localhost:9090)")
23
+ serve_parser.add_argument("--collect-interval", type=int, default=60,
24
+ help="SLO collection + portfolio cycle interval in seconds (default: 60)")
25
+ serve_parser.add_argument("--drift-interval", type=int, default=1800,
26
+ help="Drift analysis cycle interval in seconds (default: 1800)")
27
+ serve_parser.add_argument("--topology-interval", type=int, default=86400,
28
+ help="Topology/blast-radius cycle interval in seconds (default: 86400)")
29
+ serve_parser.add_argument("--correlate-interval", type=int, default=10,
30
+ help="Correlate session window cycle interval in seconds (default: 10)")
31
+ serve_parser.add_argument("--topology-drift-interval", type=int, default=3600,
32
+ help="Correlate topology drift cycle interval in seconds (default: 3600)")
33
+ serve_parser.add_argument("--contract-interval", type=int, default=3600,
34
+ help="Correlate contract divergence cycle interval in seconds (default: 3600)")
35
+ serve_parser.add_argument("--tempo-endpoint", default=None,
36
+ help="Tempo endpoint URL (optional — topology module is no-op if not set)")
37
+ serve_parser.add_argument("--measure-interval", type=int, default=60,
38
+ help="Measure evaluation cycle interval in seconds (default: 60)")
39
+ serve_parser.add_argument("--outcome-interval", type=int, default=60,
40
+ help="Learn outcome resolution cycle interval in seconds (default: 60)")
41
+ serve_parser.add_argument("--retrospective-interval", type=int, default=30,
42
+ help="Learn retrospective cycle interval in seconds (default: 30)")
43
+ serve_parser.add_argument("--expiry-threshold-days", type=int, default=7,
44
+ help="Verdict expiry threshold in days (default: 7)")
45
+ serve_parser.add_argument("--min-resolution-age-hours", type=int, default=1,
46
+ help="Minimum verdict age before resolution attempts in hours (default: 1)")
47
+ serve_parser.add_argument("--respond-interval", type=int, default=30,
48
+ help="Respond worker cycle interval in seconds (default: 30)")
49
+
50
+ # -- gate: deploy gate evaluation (CLI-only, not a worker module) --
51
+ gate_parser = sub.add_parser("gate", help="Evaluate deployment gate for a service")
52
+ gate_parser.add_argument("--service", required=True, help="Service name")
53
+ gate_parser.add_argument("--tier", default=None,
54
+ help="Service tier (default: resolved from manifest)")
55
+ gate_parser.add_argument("--commit-sha", default=None, help="Commit SHA (informational)")
56
+ gate_parser.add_argument("--core-url", default="http://localhost:8000",
57
+ help="Core API URL (default: http://localhost:8000)")
58
+
59
+ args = parser.parse_args()
60
+
61
+ if args.command == "serve":
62
+ from nthlayer_common.api_client import CoreAPIClient
63
+ from nthlayer_workers.observe.worker import (
64
+ ObserveCollectModule,
65
+ ObserveDriftModule,
66
+ ObserveTopologyModule,
67
+ )
68
+
69
+ client = CoreAPIClient(base_url=args.core_url)
70
+ runner = ModuleRunner(
71
+ core_url=args.core_url,
72
+ instance_id=args.instance_id,
73
+ client=client,
74
+ )
75
+
76
+ collect = ObserveCollectModule(client=client, prometheus_url=args.prometheus_url)
77
+ drift = ObserveDriftModule(client=client, prometheus_url=args.prometheus_url)
78
+ topology = ObserveTopologyModule(client=client, prometheus_url=args.prometheus_url)
79
+
80
+ runner.register(collect, interval_seconds=args.collect_interval)
81
+ runner.register(drift, interval_seconds=args.drift_interval)
82
+ runner.register(topology, interval_seconds=args.topology_interval)
83
+
84
+ # P3-D: Correlate modules — session windows, topology drift, contract divergence
85
+ from nthlayer_workers.correlate.worker import (
86
+ CorrelateContractModule,
87
+ CorrelateSessionModule,
88
+ CorrelateTopologyModule,
89
+ )
90
+
91
+ trace_backend = None
92
+ if args.tempo_endpoint:
93
+ from nthlayer_workers.correlate.traces.tempo import TempoTraceBackend
94
+ trace_backend = TempoTraceBackend(endpoint=args.tempo_endpoint)
95
+
96
+ correlate_session = CorrelateSessionModule(client=client)
97
+ correlate_topology = CorrelateTopologyModule(client=client, trace_backend=trace_backend)
98
+ correlate_contract = CorrelateContractModule(client=client, prometheus_url=args.prometheus_url)
99
+
100
+ runner.register(correlate_session, interval_seconds=args.correlate_interval)
101
+ runner.register(correlate_topology, interval_seconds=args.topology_drift_interval)
102
+ runner.register(correlate_contract, interval_seconds=args.contract_interval)
103
+
104
+ # P3-F: Learn modules — outcome resolution + retrospective
105
+ from nthlayer_workers.learn.worker import LearnOutcomeModule, LearnRetrospectiveModule
106
+
107
+ learn_outcome = LearnOutcomeModule(
108
+ client=client,
109
+ expiry_threshold_days=args.expiry_threshold_days,
110
+ minimum_resolution_age_hours=args.min_resolution_age_hours,
111
+ )
112
+ learn_retro = LearnRetrospectiveModule(client=client)
113
+
114
+ runner.register(learn_outcome, interval_seconds=args.outcome_interval)
115
+ runner.register(learn_retro, interval_seconds=args.retrospective_interval)
116
+
117
+ # P3-C: Measure module — judgment SLO evaluation
118
+ from nthlayer_workers.measure.worker import MeasureModule
119
+
120
+ measure = MeasureModule(client=client, prometheus_url=args.prometheus_url)
121
+ runner.register(measure, interval_seconds=args.measure_interval)
122
+
123
+ # P3-E: Respond module — incident response coordinator
124
+ from nthlayer_workers.respond.config import RespondConfig
125
+ from nthlayer_workers.respond.worker import RespondModule
126
+
127
+ respond_config = RespondConfig(cycle_interval_seconds=float(args.respond_interval))
128
+ respond = RespondModule(client=client, config=respond_config)
129
+ runner.register(respond, interval_seconds=args.respond_interval)
130
+
131
+ asyncio.run(runner.run())
132
+
133
+ elif args.command == "gate":
134
+ exit_code = _run_gate(args)
135
+ sys.exit(exit_code)
136
+
137
+ else:
138
+ parser.print_help()
139
+ sys.exit(1)
140
+
141
+
142
+ def _run_gate(args: argparse.Namespace) -> int:
143
+ """Run deploy gate evaluation.
144
+
145
+ Exit codes:
146
+ 0 = APPROVED or WARNING (deploy allowed; WARNING text on stderr)
147
+ 1 = evaluation error (couldn't evaluate; deploy scripts decide)
148
+ 2 = BLOCKED (do not deploy)
149
+ """
150
+ return asyncio.run(_gate_async(args))
151
+
152
+
153
+ async def _gate_async(args: argparse.Namespace) -> int:
154
+ """Async gate evaluation — single event loop for all core API calls."""
155
+ import json
156
+
157
+ from nthlayer_common.api_client import CoreAPIClient
158
+
159
+ from nthlayer_workers.observe.assessment import Assessment, create, from_dict, to_dict
160
+ from nthlayer_workers.observe.gate.evaluator import check_deploy
161
+ from nthlayer_workers.observe.store import AssessmentFilter, MemoryAssessmentStore
162
+
163
+ client = CoreAPIClient(base_url=args.core_url)
164
+
165
+ try:
166
+ # 1. Resolve tier from manifest if not specified
167
+ tier = args.tier
168
+ if tier is None:
169
+ manifest_result = await client.get_manifest(args.service)
170
+ if not manifest_result.ok:
171
+ print(f"Error: could not fetch manifest for {args.service}: {manifest_result.error}", file=sys.stderr)
172
+ return 1
173
+ if not isinstance(manifest_result.data, dict):
174
+ print(f"Error: malformed manifest response for {args.service}", file=sys.stderr)
175
+ return 1
176
+ tier = manifest_result.data.get("tier", "standard")
177
+
178
+ # 2. Fetch SLO assessments from core into a MemoryAssessmentStore
179
+ # Single fetch — used by both check_deploy and parent_ids extraction.
180
+ api_result = await client.get_assessments(service=args.service, kind="slo_status")
181
+ if not api_result.ok:
182
+ print(f"Error: could not fetch assessments: {api_result.error}", file=sys.stderr)
183
+ return 1
184
+
185
+ store = MemoryAssessmentStore()
186
+ slo_ids: list[str] = []
187
+ for d in (api_result.data or []):
188
+ a = from_dict(d)
189
+ store.put(a)
190
+ slo_ids.append(a.id)
191
+
192
+ # 3. Evaluate gate
193
+ result = check_deploy(args.service, tier, store)
194
+
195
+ # 4. Submit deploy_gate assessment to core
196
+ assessment = create("deploy_gate", args.service, {
197
+ "action": "deploy",
198
+ "decision": result.result.name.lower(),
199
+ "budget_remaining_pct": result.budget_remaining_pct,
200
+ "warning_threshold": result.warning_threshold,
201
+ "blocking_threshold": result.blocking_threshold,
202
+ "slo_count": result.slo_count,
203
+ "reasons": [result.message] + result.recommendations,
204
+ "commit_sha": args.commit_sha,
205
+ "parent_ids": slo_ids,
206
+ })
207
+ await client.submit_assessment(to_dict(assessment))
208
+
209
+ # 5. Output result
210
+ output = {
211
+ "service": result.service,
212
+ "tier": result.tier,
213
+ "decision": result.result.name,
214
+ "budget_remaining_pct": round(result.budget_remaining_pct, 1),
215
+ "message": result.message,
216
+ "recommendations": result.recommendations,
217
+ }
218
+ print(json.dumps(output, indent=2))
219
+
220
+ if result.result.name == "BLOCKED":
221
+ return 2
222
+ if result.result.name == "WARNING":
223
+ print(f"WARNING: {result.message}", file=sys.stderr)
224
+ return 0
225
+
226
+ except Exception as e:
227
+ print(f"Error: gate evaluation failed: {e}", file=sys.stderr)
228
+ return 1
229
+ finally:
230
+ await client.close()
231
+
232
+
233
+ if __name__ == "__main__":
234
+ main()
@@ -0,0 +1 @@
1
+ """SitRep — Situational awareness through automated signal correlation."""