sum-engine 0.5.0__tar.gz → 0.6.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 (119) hide show
  1. {sum_engine-0.5.0 → sum_engine-0.6.0}/PKG-INFO +8 -6
  2. {sum_engine-0.5.0 → sum_engine-0.6.0}/README.md +2 -2
  3. {sum_engine-0.5.0 → sum_engine-0.6.0}/pyproject.toml +14 -5
  4. sum_engine-0.6.0/sum_cli/audit_log.py +171 -0
  5. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_cli/main.py +265 -1
  6. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine.egg-info/PKG-INFO +8 -6
  7. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine.egg-info/SOURCES.txt +42 -0
  8. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine.egg-info/requires.txt +6 -3
  9. sum_engine-0.6.0/sum_engine_internal/agent_surface/__init__.py +22 -0
  10. sum_engine-0.6.0/sum_engine_internal/agent_surface/bind.py +138 -0
  11. sum_engine-0.6.0/sum_engine_internal/agent_surface/mcp_bind.py +677 -0
  12. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/algorithms/syntactic_sieve.py +9 -1
  13. sum_engine-0.6.0/sum_engine_internal/compliance/__init__.py +48 -0
  14. sum_engine-0.6.0/sum_engine_internal/compliance/_predicates.py +86 -0
  15. sum_engine-0.6.0/sum_engine_internal/compliance/eu_ai_act_article_12.py +197 -0
  16. sum_engine-0.6.0/sum_engine_internal/compliance/gdpr_article_30.py +207 -0
  17. sum_engine-0.6.0/sum_engine_internal/compliance/hipaa_164_312_b.py +230 -0
  18. sum_engine-0.6.0/sum_engine_internal/compliance/iso_27001_8_15.py +160 -0
  19. sum_engine-0.6.0/sum_engine_internal/compliance/pci_dss_4_req_10.py +264 -0
  20. sum_engine-0.6.0/sum_engine_internal/compliance/report.py +77 -0
  21. sum_engine-0.6.0/sum_engine_internal/compliance/soc_2_cc_7_2.py +153 -0
  22. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/ensemble/llm_dispatch.py +183 -16
  23. sum_engine-0.6.0/sum_engine_internal/evidence/__init__.py +45 -0
  24. sum_engine-0.6.0/sum_engine_internal/evidence/chain.py +160 -0
  25. sum_engine-0.6.0/sum_engine_internal/graph_store/__init__.py +26 -0
  26. sum_engine-0.6.0/sum_engine_internal/graph_store/base.py +105 -0
  27. sum_engine-0.6.0/sum_engine_internal/graph_store/egglog_store.py +360 -0
  28. sum_engine-0.6.0/sum_engine_internal/graph_store/unionfind_store.py +206 -0
  29. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/infrastructure/akashic_ledger.py +34 -11
  30. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/infrastructure/canonical_codec.py +374 -0
  31. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/mcp_server/server.py +23 -1
  32. sum_engine-0.6.0/sum_engine_internal/research/bootstrap/__init__.py +41 -0
  33. sum_engine-0.6.0/sum_engine_internal/research/bootstrap/multiplier_bootstrap.py +181 -0
  34. sum_engine-0.6.0/sum_engine_internal/research/conformal/__init__.py +44 -0
  35. sum_engine-0.6.0/sum_engine_internal/research/conformal/entropy_baseline.py +182 -0
  36. sum_engine-0.6.0/sum_engine_internal/research/conformal/split_conformal.py +184 -0
  37. sum_engine-0.6.0/sum_engine_internal/research/lsh/__init__.py +19 -0
  38. sum_engine-0.6.0/sum_engine_internal/research/lsh/bundle_index.py +179 -0
  39. sum_engine-0.6.0/sum_engine_internal/research/mmd/__init__.py +54 -0
  40. sum_engine-0.6.0/sum_engine_internal/research/mmd/baseline.py +328 -0
  41. sum_engine-0.6.0/sum_engine_internal/research/mmd/mmd.py +184 -0
  42. sum_engine-0.6.0/sum_engine_internal/research/robust_pca/__init__.py +35 -0
  43. sum_engine-0.6.0/sum_engine_internal/research/robust_pca/axiom_embedding.py +69 -0
  44. sum_engine-0.6.0/sum_engine_internal/research/robust_pca/pcp.py +173 -0
  45. sum_engine-0.6.0/sum_engine_internal/research/sequential/__init__.py +21 -0
  46. sum_engine-0.6.0/sum_engine_internal/research/sequential/sprt.py +183 -0
  47. sum_engine-0.6.0/sum_engine_internal/research/sheaf_laplacian_v2.py +649 -0
  48. sum_engine-0.6.0/sum_engine_internal/research/sheaf_laplacian_v3.py +509 -0
  49. sum_engine-0.6.0/sum_engine_internal/research/sheaf_laplacian_v32.py +170 -0
  50. sum_engine-0.6.0/sum_engine_internal/research/smt_consistency/__init__.py +34 -0
  51. sum_engine-0.6.0/sum_engine_internal/research/smt_consistency/consistency.py +188 -0
  52. sum_engine-0.6.0/sum_engine_internal/research/smt_consistency/predicate_library.py +77 -0
  53. sum_engine-0.6.0/sum_engine_internal/research/spectral_entropy/__init__.py +41 -0
  54. sum_engine-0.6.0/sum_engine_internal/research/spectral_entropy/vn_entropy.py +141 -0
  55. {sum_engine-0.5.0 → sum_engine-0.6.0}/LICENSE +0 -0
  56. {sum_engine-0.5.0 → sum_engine-0.6.0}/setup.cfg +0 -0
  57. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_cli/__init__.py +0 -0
  58. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine.egg-info/dependency_links.txt +0 -0
  59. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine.egg-info/entry_points.txt +0 -0
  60. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine.egg-info/top_level.txt +0 -0
  61. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/__init__.py +0 -0
  62. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/adapters/__init__.py +0 -0
  63. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/adapters/format_pivot.py +0 -0
  64. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/algorithms/__init__.py +0 -0
  65. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/algorithms/causal_discovery.py +0 -0
  66. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/algorithms/chunked_corpus.py +0 -0
  67. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/algorithms/minhash.py +0 -0
  68. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/algorithms/predicate_canon.py +0 -0
  69. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/algorithms/semantic_arithmetic.py +0 -0
  70. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/algorithms/zk_semantics.py +0 -0
  71. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/ensemble/__init__.py +0 -0
  72. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/ensemble/automated_scientist.py +0 -0
  73. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/ensemble/autonomous_agent.py +0 -0
  74. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/ensemble/causal_triggers.py +0 -0
  75. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/ensemble/confidence_calibrator.py +0 -0
  76. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/ensemble/data/__init__.py +0 -0
  77. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/ensemble/data/common_english_2000.txt +0 -0
  78. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/ensemble/data/common_english_5000.txt +0 -0
  79. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/ensemble/epistemic_arbiter.py +0 -0
  80. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/ensemble/epistemic_loop.py +0 -0
  81. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/ensemble/extraction_validator.py +0 -0
  82. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/ensemble/gauge_orchestrator.py +0 -0
  83. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/ensemble/live_llm_adapter.py +0 -0
  84. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/ensemble/llm_entailment.py +0 -0
  85. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/ensemble/mass_semantic_engine.py +0 -0
  86. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/ensemble/ouroboros.py +0 -0
  87. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/ensemble/s25_interventions.py +0 -0
  88. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/ensemble/semantic_dedup.py +0 -0
  89. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/ensemble/slider_renderer.py +0 -0
  90. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/ensemble/tome_generator.py +0 -0
  91. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/ensemble/tome_sliders.py +0 -0
  92. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/ensemble/vector_bridge.py +0 -0
  93. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/ensemble/venn_abers.py +0 -0
  94. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/infrastructure/__init__.py +0 -0
  95. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/infrastructure/jcs.py +0 -0
  96. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/infrastructure/jose_envelope.py +0 -0
  97. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/infrastructure/key_manager.py +0 -0
  98. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/infrastructure/p2p_mesh.py +0 -0
  99. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/infrastructure/prov_o.py +0 -0
  100. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/infrastructure/provenance.py +0 -0
  101. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/infrastructure/rate_limiter.py +0 -0
  102. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/infrastructure/resource_guards.py +0 -0
  103. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/infrastructure/scheme_registry.py +0 -0
  104. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/infrastructure/state_encoding.py +0 -0
  105. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/infrastructure/telemetry.py +0 -0
  106. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/infrastructure/tome_parser.py +0 -0
  107. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/infrastructure/verifiable_credential.py +0 -0
  108. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/infrastructure/zig_bridge.py +0 -0
  109. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/mcp_server/__init__.py +0 -0
  110. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/mcp_server/__main__.py +0 -0
  111. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/mcp_server/errors.py +0 -0
  112. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/merkle_sidecar/__init__.py +0 -0
  113. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/merkle_sidecar/tree.py +0 -0
  114. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/render_receipt/__init__.py +0 -0
  115. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/render_receipt/verifier.py +0 -0
  116. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/research/__init__.py +0 -0
  117. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/research/sheaf_laplacian.py +0 -0
  118. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/trust_root/__init__.py +0 -0
  119. {sum_engine-0.5.0 → sum_engine-0.6.0}/sum_engine_internal/trust_root/verifier.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sum-engine
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: SUM — bidirectional knowledge distillation with optional cryptographic attestation. Pipe prose, get a CanonicalBundle (HMAC / Ed25519 / W3C VC 2.0), verify anywhere.
5
5
  Author: ototao
6
6
  License: Apache-2.0
@@ -25,9 +25,11 @@ Requires-Dist: cryptography>=41.0.0
25
25
  Requires-Dist: sympy>=1.12
26
26
  Provides-Extra: sieve
27
27
  Requires-Dist: spacy>=3.7.0; extra == "sieve"
28
+ Provides-Extra: openai
29
+ Requires-Dist: openai<3.0.0,>=1.40.0; extra == "openai"
30
+ Requires-Dist: pydantic>=2.0.0; extra == "openai"
28
31
  Provides-Extra: llm
29
- Requires-Dist: openai<3.0.0,>=1.40.0; extra == "llm"
30
- Requires-Dist: pydantic>=2.0.0; extra == "llm"
32
+ Requires-Dist: sum-engine[openai]; extra == "llm"
31
33
  Provides-Extra: anthropic
32
34
  Requires-Dist: anthropic>=0.97.0; extra == "anthropic"
33
35
  Requires-Dist: pydantic>=2.0.0; extra == "anthropic"
@@ -51,7 +53,7 @@ Requires-Dist: build>=1.0.0; extra == "dev"
51
53
  Requires-Dist: hypothesis>=6.0.0; extra == "dev"
52
54
  Provides-Extra: all
53
55
  Requires-Dist: sum-engine[sieve]; extra == "all"
54
- Requires-Dist: sum-engine[llm]; extra == "all"
56
+ Requires-Dist: sum-engine[openai]; extra == "all"
55
57
  Requires-Dist: sum-engine[anthropic]; extra == "all"
56
58
  Requires-Dist: sum-engine[receipt-verify]; extra == "all"
57
59
  Requires-Dist: sum-engine[mcp]; extra == "all"
@@ -165,7 +167,7 @@ The reverse direction also runs under explicit slider control. The local path ac
165
167
  sum render --density 0.5 < bundle.json
166
168
  # → keeps the lex-prefix half of the axioms; @sliders header records what was requested
167
169
 
168
- sum render --length 0.9 --use-worker https://sum.ototao.com --json < bundle.json
170
+ sum render --length 0.9 --use-worker https://sum-demo.ototao.workers.dev --json < bundle.json
169
171
  # → LLM-conditioned tome + signed render_receipt (sum.render_receipt.v1) on stdout
170
172
  ```
171
173
 
@@ -192,7 +194,7 @@ pip install 'sum-engine[mcp,sieve]'
192
194
 
193
195
  ### Calling SUM over HTTP
194
196
 
195
- The hosted Worker at `https://sum.ototao.com` exposes `/api/render`, `/api/complete`, `/api/qid`, and the `/.well-known/{jwks,revoked-kids}.json` verification surfaces. [`docs/API_REFERENCE.md`](docs/API_REFERENCE.md) is the wire spec — request/response shapes, error codes, the six-step receipt-verification flow, working Node + Python examples. Use this when the caller is a web app, mobile app, or server-side service; use the MCP server when the caller is a local LLM client.
197
+ The hosted Worker at `https://sum-demo.ototao.workers.dev` exposes `/api/render`, `/api/complete`, `/api/qid`, and the `/.well-known/{jwks,revoked-kids}.json` verification surfaces. [`docs/API_REFERENCE.md`](docs/API_REFERENCE.md) is the wire spec — request/response shapes, error codes, the six-step receipt-verification flow, working Node + Python examples. Use this when the caller is a web app, mobile app, or server-side service; use the MCP server when the caller is a local LLM client.
196
198
 
197
199
  ---
198
200
 
@@ -104,7 +104,7 @@ The reverse direction also runs under explicit slider control. The local path ac
104
104
  sum render --density 0.5 < bundle.json
105
105
  # → keeps the lex-prefix half of the axioms; @sliders header records what was requested
106
106
 
107
- sum render --length 0.9 --use-worker https://sum.ototao.com --json < bundle.json
107
+ sum render --length 0.9 --use-worker https://sum-demo.ototao.workers.dev --json < bundle.json
108
108
  # → LLM-conditioned tome + signed render_receipt (sum.render_receipt.v1) on stdout
109
109
  ```
110
110
 
@@ -131,7 +131,7 @@ pip install 'sum-engine[mcp,sieve]'
131
131
 
132
132
  ### Calling SUM over HTTP
133
133
 
134
- The hosted Worker at `https://sum.ototao.com` exposes `/api/render`, `/api/complete`, `/api/qid`, and the `/.well-known/{jwks,revoked-kids}.json` verification surfaces. [`docs/API_REFERENCE.md`](docs/API_REFERENCE.md) is the wire spec — request/response shapes, error codes, the six-step receipt-verification flow, working Node + Python examples. Use this when the caller is a web app, mobile app, or server-side service; use the MCP server when the caller is a local LLM client.
134
+ The hosted Worker at `https://sum-demo.ototao.workers.dev` exposes `/api/render`, `/api/complete`, `/api/qid`, and the `/.well-known/{jwks,revoked-kids}.json` verification surfaces. [`docs/API_REFERENCE.md`](docs/API_REFERENCE.md) is the wire spec — request/response shapes, error codes, the six-step receipt-verification flow, working Node + Python examples. Use this when the caller is a web app, mobile app, or server-side service; use the MCP server when the caller is a local LLM client.
135
135
 
136
136
  ---
137
137
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sum-engine"
7
- version = "0.5.0"
7
+ version = "0.6.0"
8
8
  description = "SUM — bidirectional knowledge distillation with optional cryptographic attestation. Pipe prose, get a CanonicalBundle (HMAC / Ed25519 / W3C VC 2.0), verify anywhere."
9
9
  readme = "README.md"
10
10
  license = { text = "Apache-2.0" }
@@ -39,10 +39,19 @@ dependencies = [
39
39
  [project.optional-dependencies]
40
40
  # Dependency-injection extras. Users pick the extractor they want:
41
41
  # pip install sum-engine[sieve] # deterministic, offline spaCy path
42
- # pip install sum-engine[llm] # OpenAI structured-output path
43
- # pip install sum-engine[all] # both, plus dev tooling
42
+ # pip install sum-engine[openai] # OpenAI structured-output path
43
+ # pip install sum-engine[llm] # alias for [openai] (legacy name)
44
+ # pip install sum-engine[all] # everything, plus dev tooling
44
45
  sieve = ["spacy>=3.7.0"]
45
- llm = ["openai>=1.40.0,<3.0.0", "pydantic>=2.0.0"]
46
+ # `[openai]` is the canonical, vendor-named extra; `[llm]` is kept as a
47
+ # back-compat alias because it predates the multi-provider dispatcher
48
+ # (Anthropic and OpenAI now have their own named extras). Both install
49
+ # identical dependencies — the openai SDK plus pydantic for structured
50
+ # outputs. The vendor adapter is at
51
+ # sum_engine_internal/ensemble/llm_dispatch.py::OpenAIAdapter; the
52
+ # render-path TS companion is worker/src/routes/render.ts::callOpenAI.
53
+ openai = ["openai>=1.40.0,<3.0.0", "pydantic>=2.0.0"]
54
+ llm = ["sum-engine[openai]"]
46
55
  # Anthropic Messages-API adapter (Claude family). Installed alongside
47
56
  # [llm] for the §2.5 frontier-LLM benchmark runs that compare
48
57
  # generator-side interventions across providers. The runner picks
@@ -107,7 +116,7 @@ dev = [
107
116
  ]
108
117
  all = [
109
118
  "sum-engine[sieve]",
110
- "sum-engine[llm]",
119
+ "sum-engine[openai]",
111
120
  "sum-engine[anthropic]",
112
121
  "sum-engine[receipt-verify]",
113
122
  "sum-engine[mcp]",
@@ -0,0 +1,171 @@
1
+ """sum_cli.audit_log — universal audit-log streaming for compliance.
2
+
3
+ When the ``SUM_AUDIT_LOG`` environment variable is set to a file path,
4
+ every ``sum attest`` / ``sum verify`` / ``sum render`` operation
5
+ appends a single JSONL row to that file describing what was done.
6
+
7
+ This is the regime-agnostic foundation of the compliance primitives
8
+ direction (Path 3). A specific regime (GDPR-Art-30, HIPAA-164.514,
9
+ EU AI Act Annex IV, etc.) can be implemented as a downstream
10
+ consumer of this audit log: tail the file, validate per-regime
11
+ required fields, raise on policy violation. The audit log itself
12
+ makes no regime-specific assumptions — it records *what happened*
13
+ verbatim so any auditor can reconstruct the operation.
14
+
15
+ Schema: ``sum.audit_log.v1`` — additive; new optional fields may
16
+ appear in future minor versions; consumers should ignore unknown
17
+ keys.
18
+
19
+ Required fields per row:
20
+ - ``schema`` = ``"sum.audit_log.v1"``
21
+ - ``timestamp`` (ISO 8601 UTC, e.g. ``"2026-05-01T18:35:14.123Z"``)
22
+ - ``operation`` ∈ ``{"attest", "verify", "render"}``
23
+ - ``cli_version`` (the ``sum-cli`` version that produced the row)
24
+
25
+ Operation-specific optional fields:
26
+ - attest: ``source_uri``, ``state_integer_digits``, ``axiom_count``,
27
+ ``extractor``, ``signed`` (bool — Ed25519 attached?),
28
+ ``hmac`` (bool), ``branch``
29
+ - verify: ``state_integer_digits``, ``axiom_count``, ``signatures``
30
+ ``{ed25519, hmac}``, ``branch``, ``ok`` (bool)
31
+ - render: ``axiom_count_input``, ``mode`` (``"local-deterministic"``
32
+ or ``"worker"``), ``sliders``, ``render_receipt_kid`` (if
33
+ worker), ``worker_url`` (if worker)
34
+
35
+ Failures (exit code != 0) emit a row with ``error`` field describing
36
+ the failure class. Audit-log writes themselves never fail loudly —
37
+ if ``SUM_AUDIT_LOG`` points at a non-writable path, the operation
38
+ proceeds and the failure is reported on stderr only when ``--verbose``
39
+ is set; the audit semantics fail-open by design (a non-functional
40
+ audit destination should not break the trust loop).
41
+
42
+ Env var precedence:
43
+ 1. ``SUM_AUDIT_LOG`` env var (path to JSONL file; appended to)
44
+ 2. unset → no audit logging
45
+
46
+ The path may be ``-`` for stdout (rare; mostly useful for piping
47
+ into another tool); otherwise treated as a file path with append-mode
48
+ open.
49
+
50
+ Concurrency: writes are O_APPEND on POSIX, so multiple sum
51
+ processes writing to the same audit log produce a serialised
52
+ ordering (atomic per write() up to PIPE_BUF on most systems —
53
+ single-line JSONL records well under that bound). We write each
54
+ row in one ``f.write()`` call to maximise atomicity.
55
+ """
56
+ from __future__ import annotations
57
+
58
+ import json
59
+ import os
60
+ import sys
61
+ from datetime import datetime, timezone
62
+ from typing import Any
63
+
64
+
65
+ _AUDIT_LOG_SCHEMA = "sum.audit_log.v1"
66
+
67
+
68
+ def _audit_log_path() -> str | None:
69
+ """Return the configured audit-log destination, or None if unset."""
70
+ p = os.environ.get("SUM_AUDIT_LOG")
71
+ if p is None or p == "":
72
+ return None
73
+ return p
74
+
75
+
76
+ def _identity_fields() -> dict[str, Any]:
77
+ """Optional identity fields populated from env vars.
78
+
79
+ Sprint 4 of the intensification path to arXiv (PR #140). Closes
80
+ the PCI DSS Req 10.2.2 user-identification gap named in
81
+ ``docs/COMPLIANCE_PCI_DSS_4_REQ_10.md``. Three env vars are
82
+ consulted; each populated value becomes an optional field on
83
+ the audit-log row:
84
+
85
+ - ``SUM_AUDIT_USER_ID`` → ``user_id`` field. Per PCI Req 10.2.2,
86
+ "user identification" is the FIRST required field for each
87
+ audit-log event. Operators running SUM behind an
88
+ authenticating proxy populate this from the proxy's session
89
+ identity at process start.
90
+ - ``SUM_AUDIT_HOST_ID`` → ``host_id`` field. Multi-host
91
+ deployments (clusters, k8s pods, container fleets) use this
92
+ to attribute events to specific compute units.
93
+ - ``SUM_AUDIT_IP_ADDRESS`` → ``ip_address`` field. Network-
94
+ layer origination, useful for incident-response / forensic
95
+ analysis under PCI Req 10.2.2's "origination of event"
96
+ requirement.
97
+
98
+ All three are *optional*. An unset env var produces an absent
99
+ field (not a null value). The audit-log schema stays at
100
+ ``sum.audit_log.v1``; these are additive optional fields under
101
+ the existing schema's "consumers should ignore unknown keys"
102
+ convention. Backward compat: rows without these fields still
103
+ pass every existing validator.
104
+
105
+ PCI DSS validator R7 (``pci-dss-4-req-10.user-identification-
106
+ recommended``, added in the same PR as this function) treats
107
+ a missing ``user_id`` as a Req 10.2.2 violation in compliance-
108
+ mode runs.
109
+ """
110
+ out: dict[str, Any] = {}
111
+ for env_var, field in (
112
+ ("SUM_AUDIT_USER_ID", "user_id"),
113
+ ("SUM_AUDIT_HOST_ID", "host_id"),
114
+ ("SUM_AUDIT_IP_ADDRESS", "ip_address"),
115
+ ):
116
+ value = os.environ.get(env_var)
117
+ if value: # non-empty (skips both None and empty string)
118
+ out[field] = value
119
+ return out
120
+
121
+
122
+ def emit_audit_event(operation: str, payload: dict[str, Any]) -> None:
123
+ """Append a single JSONL row to the audit log if configured.
124
+
125
+ Fail-open: if the audit destination is unwritable, the operation
126
+ proceeds normally; the failure is silent unless verbose stderr
127
+ is enabled by the caller. Audit logging is *advisory* — the
128
+ canonical bundle / receipt still carries the load-bearing trust
129
+ properties; the audit log is for downstream compliance tools
130
+ to ingest.
131
+
132
+ Identity fields (``user_id`` / ``host_id`` / ``ip_address``) are
133
+ populated from env vars by :func:`_identity_fields`. They appear
134
+ on the row only when the corresponding env var is set; absent
135
+ env vars produce absent fields (not null values). The
136
+ ``payload`` argument takes precedence over identity fields if
137
+ a caller passes overlapping keys — useful for tests that want
138
+ to pin specific identity values without touching the
139
+ environment.
140
+ """
141
+ path = _audit_log_path()
142
+ if path is None:
143
+ return
144
+
145
+ from sum_cli import __version__
146
+
147
+ row = {
148
+ "schema": _AUDIT_LOG_SCHEMA,
149
+ "timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.")
150
+ + f"{datetime.now(timezone.utc).microsecond // 1000:03d}Z",
151
+ "operation": operation,
152
+ "cli_version": __version__,
153
+ # Identity fields from env, then payload last so the caller
154
+ # wins on overlap (intentional for test seams).
155
+ **_identity_fields(),
156
+ **payload,
157
+ }
158
+ line = json.dumps(row, separators=(",", ":")) + "\n"
159
+
160
+ try:
161
+ if path == "-":
162
+ sys.stdout.write(line)
163
+ sys.stdout.flush()
164
+ else:
165
+ with open(path, "a", encoding="utf-8") as f:
166
+ f.write(line)
167
+ except OSError:
168
+ # Fail-open: audit destination unavailable; do not block
169
+ # the operation. The canonical bundle / receipt remains the
170
+ # load-bearing trust artifact.
171
+ pass
@@ -420,6 +420,18 @@ def cmd_attest(args: argparse.Namespace) -> int:
420
420
  f"{len(bundle['state_integer'])} digits",
421
421
  file=sys.stderr,
422
422
  )
423
+
424
+ from sum_cli.audit_log import emit_audit_event
425
+ emit_audit_event("attest", {
426
+ "source_uri": source_uri,
427
+ "axiom_count": len(triples),
428
+ "state_integer_digits": len(bundle["state_integer"]),
429
+ "extractor": extractor,
430
+ "branch": args.branch,
431
+ "signed": "public_signature" in bundle,
432
+ "hmac": "signature" in bundle,
433
+ "input_format": format_sidecar.get("input_format"),
434
+ })
423
435
  return 0
424
436
 
425
437
 
@@ -844,6 +856,16 @@ def cmd_verify(args: argparse.Namespace) -> int:
844
856
  f"sum: ✓ verified {axioms} axiom(s), state integer matches ({marks})",
845
857
  file=sys.stderr,
846
858
  )
859
+
860
+ from sum_cli.audit_log import emit_audit_event
861
+ emit_audit_event("verify", {
862
+ "ok": True,
863
+ "axiom_count": axioms,
864
+ "state_integer_digits": len(claimed_state_str),
865
+ "branch": bundle.get("branch", "main"),
866
+ "signatures": {"hmac": hmac_status, "ed25519": ed25519_status},
867
+ "extraction": extraction,
868
+ })
847
869
  return 0
848
870
 
849
871
 
@@ -926,7 +948,15 @@ def _post_render_to_worker(
926
948
  endpoint,
927
949
  data=payload,
928
950
  method="POST",
929
- headers={"content-type": "application/json"},
951
+ headers={
952
+ "content-type": "application/json",
953
+ # Cloudflare in front of hosted Workers (incl. the
954
+ # default sum-demo.ototao.workers.dev) rejects the
955
+ # default Python urllib User-Agent with HTTP 403 /
956
+ # error 1010. Identify ourselves as a known scripted
957
+ # client so the request passes the bot-detection layer.
958
+ "user-agent": f"sum-cli/{__version__} (+https://github.com/OtotaO/SUM)",
959
+ },
930
960
  )
931
961
  try:
932
962
  with urllib.request.urlopen(req, timeout=60) as resp:
@@ -1130,6 +1160,20 @@ def _emit_render_output(envelope: dict, args: argparse.Namespace) -> int:
1130
1160
  kid = envelope["render_receipt"].get("kid", "?")
1131
1161
  msg += f", receipt kid={kid}"
1132
1162
  print(msg, file=sys.stderr)
1163
+
1164
+ from sum_cli.audit_log import emit_audit_event
1165
+ audit_payload: dict = {
1166
+ "mode": envelope.get("mode"),
1167
+ "axiom_count_input": envelope.get("axiom_count_input"),
1168
+ "tome_chars": len(tome_text),
1169
+ "sliders": envelope.get("sliders"),
1170
+ }
1171
+ if "render_receipt" in envelope:
1172
+ receipt = envelope["render_receipt"]
1173
+ audit_payload["render_receipt_kid"] = receipt.get("kid")
1174
+ audit_payload["render_receipt_schema"] = receipt.get("schema")
1175
+ audit_payload["worker_url"] = envelope.get("worker_url")
1176
+ emit_audit_event("render", audit_payload)
1133
1177
  return 0
1134
1178
 
1135
1179
 
@@ -1499,6 +1543,172 @@ def cmd_schema(args: argparse.Namespace) -> int:
1499
1543
  return 0
1500
1544
 
1501
1545
 
1546
+ # ─── compliance ──────────────────────────────────────────────────────
1547
+ #
1548
+ # Per-regime validators consuming sum.audit_log.v1. The audit log is
1549
+ # regime-agnostic substrate; ``sum compliance check`` is the actionable
1550
+ # layer that turns it into a pass/fail verdict for a specific regime.
1551
+ # Exit code 0 when ok=true, 1 otherwise — pipe-friendly for CI gates.
1552
+
1553
+ _COMPLIANCE_REGIMES: dict[str, str] = {
1554
+ "eu-ai-act-article-12": (
1555
+ "EU AI Act (Regulation (EU) 2024/1689) Article 12 — record-"
1556
+ "keeping for high-risk AI systems. Pins per-row traceability "
1557
+ "fields (timestamp, operation, cli_version), schema, and "
1558
+ "operation-specific anchors (source_uri / axiom_count / "
1559
+ "state_integer_digits / mode)."
1560
+ ),
1561
+ "gdpr-article-30": (
1562
+ "GDPR (Regulation (EU) 2016/679) Article 30 — Records of "
1563
+ "Processing Activities. Pins the per-row floor enabling Art "
1564
+ "30 reporting (schema, timestamp, ISO-8601-UTC parseability, "
1565
+ "processing-category indicator, processor identity). The "
1566
+ "controller separately maintains record-set-scope metadata "
1567
+ "(Art 30(1)(a)–(g) controller name, purposes, categories, "
1568
+ "recipients, transfers, retention, security measures) "
1569
+ "out-of-band; this validator does not pin those."
1570
+ ),
1571
+ "hipaa-164-312-b": (
1572
+ "HIPAA Security Rule 45 CFR § 164.312(b) — Audit Controls "
1573
+ "(Technical Safeguards). Pins the per-row form requirements "
1574
+ "for an audit recording that supports examination of "
1575
+ "activity (schema, timestamp, ISO-8601-UTC, activity type, "
1576
+ "system component identification, per-operation examination "
1577
+ "anchors). Deployment-scope obligations — auditor function, "
1578
+ "retention, ePHI inventory — live outside this validator."
1579
+ ),
1580
+ "iso-27001-8-15": (
1581
+ "ISO/IEC 27001:2022 Annex A.8.15 — Logging. Pins the per-"
1582
+ "row form floor an audit log must satisfy for the recording "
1583
+ "to count as a 'produced' log under A.8.15 (schema, "
1584
+ "timestamp, ISO-8601-UTC, activity, system component). The "
1585
+ "'stored', 'protected', 'analysed' verbs map to deployment-"
1586
+ "scope obligations (file-system policy, access control, "
1587
+ "SIEM integration) outside this validator."
1588
+ ),
1589
+ "soc-2-cc-7-2": (
1590
+ "SOC 2 Trust Services Criteria CC7.2 — System Operations. "
1591
+ "Pins the per-row form floor required to enable the "
1592
+ "monitoring criterion (schema, timestamp, ISO-8601-UTC, "
1593
+ "activity classification, system component identification). "
1594
+ "The detection / monitoring / analysis activities themselves "
1595
+ "(SIEM rules, alert routing, oncall rotations) live at "
1596
+ "deployment scope outside this validator."
1597
+ ),
1598
+ "pci-dss-4-req-10": (
1599
+ "PCI DSS v4.0 Requirement 10 — Log and Monitor All Access "
1600
+ "to System Components and Cardholder Data. Pins per-row "
1601
+ "content visible in audit_log.v1 against Req 10.2.2 (event "
1602
+ "content) plus 10.6 (consistent time): schema, timestamp, "
1603
+ "ISO-8601-UTC, event type, origination, event-content "
1604
+ "completeness. NOT pinned: 10.1 organisational policies; "
1605
+ "10.2.1.* specific event-type coverage; 10.2.2 user "
1606
+ "identification (audit_log.v1 has no user_id field); 10.3 "
1607
+ "log file protection; 10.4 log review process; 10.5 12-"
1608
+ "month retention; 10.7 failure detection / alerting."
1609
+ ),
1610
+ }
1611
+
1612
+
1613
+ def _compliance_validators():
1614
+ """Return the regime → validate-callable dispatch table.
1615
+
1616
+ Built lazily to avoid importing compliance modules at CLI
1617
+ startup (each module imports the dataclass infrastructure;
1618
+ deferring keeps `sum --help` cold-start fast). Registered
1619
+ regimes must match :data:`_COMPLIANCE_REGIMES` keys exactly —
1620
+ a mismatch surfaces as a KeyError at dispatch, intentional
1621
+ (better than silent fallthrough)."""
1622
+ from sum_engine_internal.compliance import ( # local import — see docstring
1623
+ eu_ai_act_article_12,
1624
+ gdpr_article_30,
1625
+ hipaa_164_312_b,
1626
+ iso_27001_8_15,
1627
+ pci_dss_4_req_10,
1628
+ soc_2_cc_7_2,
1629
+ )
1630
+ return {
1631
+ "eu-ai-act-article-12": eu_ai_act_article_12.validate,
1632
+ "gdpr-article-30": gdpr_article_30.validate,
1633
+ "hipaa-164-312-b": hipaa_164_312_b.validate,
1634
+ "iso-27001-8-15": iso_27001_8_15.validate,
1635
+ "soc-2-cc-7-2": soc_2_cc_7_2.validate,
1636
+ "pci-dss-4-req-10": pci_dss_4_req_10.validate,
1637
+ }
1638
+
1639
+
1640
+ def cmd_compliance_check(args: argparse.Namespace) -> int:
1641
+ if args.regime not in _COMPLIANCE_REGIMES:
1642
+ print(
1643
+ f"sum: unknown regime {args.regime!r}. Known: "
1644
+ f"{sorted(_COMPLIANCE_REGIMES)}",
1645
+ file=sys.stderr,
1646
+ )
1647
+ return 2
1648
+
1649
+ if args.audit_log == "-":
1650
+ text = sys.stdin.read()
1651
+ else:
1652
+ try:
1653
+ with open(args.audit_log, "r", encoding="utf-8") as f:
1654
+ text = f.read()
1655
+ except OSError as e:
1656
+ print(f"sum: cannot read --audit-log {args.audit_log!r}: {e}", file=sys.stderr)
1657
+ return 2
1658
+
1659
+ rows: list[dict] = []
1660
+ parse_errors: list[tuple[int, str]] = []
1661
+ for i, line in enumerate(text.splitlines()):
1662
+ line = line.strip()
1663
+ if not line:
1664
+ continue
1665
+ try:
1666
+ rows.append(json.loads(line))
1667
+ except json.JSONDecodeError as e:
1668
+ parse_errors.append((i, str(e)))
1669
+
1670
+ validators = _compliance_validators()
1671
+ try:
1672
+ validate = validators[args.regime]
1673
+ except KeyError:
1674
+ # Defensive — _COMPLIANCE_REGIMES gate above should have caught this.
1675
+ # Reaching here means a regime is registered in _COMPLIANCE_REGIMES
1676
+ # but missing from _compliance_validators() — a wiring drift.
1677
+ print(
1678
+ f"sum: regime {args.regime!r} listed but not wired "
1679
+ f"(internal: missing from _compliance_validators dispatch)",
1680
+ file=sys.stderr,
1681
+ )
1682
+ return 2
1683
+ report = validate(rows)
1684
+
1685
+ out = report.to_dict()
1686
+ if parse_errors:
1687
+ # Surface parse errors alongside rule violations — both are
1688
+ # compliance-relevant. A malformed JSONL line is itself a
1689
+ # traceability defect.
1690
+ out["parse_errors"] = [
1691
+ {"line_index": idx, "error": msg} for idx, msg in parse_errors
1692
+ ]
1693
+
1694
+ json.dump(out, sys.stdout, indent=2 if args.pretty else None)
1695
+ sys.stdout.write("\n")
1696
+ return 0 if (report.ok and not parse_errors) else 1
1697
+
1698
+
1699
+ def cmd_compliance_regimes(args: argparse.Namespace) -> int:
1700
+ out = {
1701
+ "schema": "sum.compliance_regimes.v1",
1702
+ "regimes": [
1703
+ {"id": rid, "description": desc}
1704
+ for rid, desc in sorted(_COMPLIANCE_REGIMES.items())
1705
+ ],
1706
+ }
1707
+ json.dump(out, sys.stdout, indent=2)
1708
+ sys.stdout.write("\n")
1709
+ return 0
1710
+
1711
+
1502
1712
  # ─── Argparse wiring ─────────────────────────────────────────────────
1503
1713
 
1504
1714
  def build_parser() -> argparse.ArgumentParser:
@@ -1878,6 +2088,60 @@ def build_parser() -> argparse.ArgumentParser:
1878
2088
  )
1879
2089
  p_schema.set_defaults(func=cmd_schema)
1880
2090
 
2091
+ # compliance — regime validators consuming sum.audit_log.v1.
2092
+ p_compliance = subparsers.add_parser(
2093
+ "compliance",
2094
+ help="Validate a sum.audit_log.v1 stream against a compliance regime.",
2095
+ description=(
2096
+ "Apply a per-regime validator to a sum.audit_log.v1 JSONL stream "
2097
+ "and emit a sum.compliance_report.v1 verdict. The audit log is "
2098
+ "regime-agnostic substrate; this verb is the actionable layer "
2099
+ "that turns it into a compliance-grade pass/fail."
2100
+ ),
2101
+ )
2102
+ p_compliance_sub = p_compliance.add_subparsers(
2103
+ dest="compliance_cmd", required=True, metavar="<compliance-cmd>",
2104
+ )
2105
+
2106
+ p_comp_check = p_compliance_sub.add_parser(
2107
+ "check",
2108
+ help="Validate an audit-log stream against a regime; emit a JSON report.",
2109
+ description=(
2110
+ "Read a sum.audit_log.v1 JSONL stream from --audit-log (or stdin "
2111
+ "if '-'), validate against the named regime, emit a "
2112
+ "sum.compliance_report.v1 JSON object on stdout. Exit code is "
2113
+ "0 when ok=true, 1 otherwise — pipe-friendly for CI gates."
2114
+ ),
2115
+ )
2116
+ p_comp_check.add_argument(
2117
+ "--regime",
2118
+ required=True,
2119
+ choices=["eu-ai-act-article-12"],
2120
+ help="Compliance regime to validate against.",
2121
+ )
2122
+ p_comp_check.add_argument(
2123
+ "--audit-log",
2124
+ required=True,
2125
+ help="Path to a sum.audit_log.v1 JSONL file ('-' for stdin).",
2126
+ )
2127
+ p_comp_check.add_argument(
2128
+ "--pretty",
2129
+ action="store_true",
2130
+ help="Pretty-print the report JSON.",
2131
+ )
2132
+ p_comp_check.set_defaults(func=cmd_compliance_check)
2133
+
2134
+ p_comp_regimes = p_compliance_sub.add_parser(
2135
+ "regimes",
2136
+ help="List available compliance regimes.",
2137
+ description=(
2138
+ "Emit the set of regime identifiers this CLI can validate "
2139
+ "against. Adding a new regime appends to this list; "
2140
+ "existing identifiers are stable."
2141
+ ),
2142
+ )
2143
+ p_comp_regimes.set_defaults(func=cmd_compliance_regimes)
2144
+
1881
2145
  return parser
1882
2146
 
1883
2147
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sum-engine
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: SUM — bidirectional knowledge distillation with optional cryptographic attestation. Pipe prose, get a CanonicalBundle (HMAC / Ed25519 / W3C VC 2.0), verify anywhere.
5
5
  Author: ototao
6
6
  License: Apache-2.0
@@ -25,9 +25,11 @@ Requires-Dist: cryptography>=41.0.0
25
25
  Requires-Dist: sympy>=1.12
26
26
  Provides-Extra: sieve
27
27
  Requires-Dist: spacy>=3.7.0; extra == "sieve"
28
+ Provides-Extra: openai
29
+ Requires-Dist: openai<3.0.0,>=1.40.0; extra == "openai"
30
+ Requires-Dist: pydantic>=2.0.0; extra == "openai"
28
31
  Provides-Extra: llm
29
- Requires-Dist: openai<3.0.0,>=1.40.0; extra == "llm"
30
- Requires-Dist: pydantic>=2.0.0; extra == "llm"
32
+ Requires-Dist: sum-engine[openai]; extra == "llm"
31
33
  Provides-Extra: anthropic
32
34
  Requires-Dist: anthropic>=0.97.0; extra == "anthropic"
33
35
  Requires-Dist: pydantic>=2.0.0; extra == "anthropic"
@@ -51,7 +53,7 @@ Requires-Dist: build>=1.0.0; extra == "dev"
51
53
  Requires-Dist: hypothesis>=6.0.0; extra == "dev"
52
54
  Provides-Extra: all
53
55
  Requires-Dist: sum-engine[sieve]; extra == "all"
54
- Requires-Dist: sum-engine[llm]; extra == "all"
56
+ Requires-Dist: sum-engine[openai]; extra == "all"
55
57
  Requires-Dist: sum-engine[anthropic]; extra == "all"
56
58
  Requires-Dist: sum-engine[receipt-verify]; extra == "all"
57
59
  Requires-Dist: sum-engine[mcp]; extra == "all"
@@ -165,7 +167,7 @@ The reverse direction also runs under explicit slider control. The local path ac
165
167
  sum render --density 0.5 < bundle.json
166
168
  # → keeps the lex-prefix half of the axioms; @sliders header records what was requested
167
169
 
168
- sum render --length 0.9 --use-worker https://sum.ototao.com --json < bundle.json
170
+ sum render --length 0.9 --use-worker https://sum-demo.ototao.workers.dev --json < bundle.json
169
171
  # → LLM-conditioned tome + signed render_receipt (sum.render_receipt.v1) on stdout
170
172
  ```
171
173
 
@@ -192,7 +194,7 @@ pip install 'sum-engine[mcp,sieve]'
192
194
 
193
195
  ### Calling SUM over HTTP
194
196
 
195
- The hosted Worker at `https://sum.ototao.com` exposes `/api/render`, `/api/complete`, `/api/qid`, and the `/.well-known/{jwks,revoked-kids}.json` verification surfaces. [`docs/API_REFERENCE.md`](docs/API_REFERENCE.md) is the wire spec — request/response shapes, error codes, the six-step receipt-verification flow, working Node + Python examples. Use this when the caller is a web app, mobile app, or server-side service; use the MCP server when the caller is a local LLM client.
197
+ The hosted Worker at `https://sum-demo.ototao.workers.dev` exposes `/api/render`, `/api/complete`, `/api/qid`, and the `/.well-known/{jwks,revoked-kids}.json` verification surfaces. [`docs/API_REFERENCE.md`](docs/API_REFERENCE.md) is the wire spec — request/response shapes, error codes, the six-step receipt-verification flow, working Node + Python examples. Use this when the caller is a web app, mobile app, or server-side service; use the MCP server when the caller is a local LLM client.
196
198
 
197
199
  ---
198
200