blun-king-cli 9.1.564 → 9.1.566

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 (88) hide show
  1. package/LIESMICH.txt +2 -2
  2. package/README.md +1 -1
  3. package/agent-spine-plugin/.claude-plugin/marketplace.json +20 -20
  4. package/agent-spine-plugin/.claude-plugin/plugin.json +14 -14
  5. package/agent-spine-plugin/.codex-plugin/plugin.json +34 -34
  6. package/agent-spine-plugin/.mcp.json +8 -8
  7. package/agent-spine-plugin/CONTRIBUTING.md +52 -0
  8. package/agent-spine-plugin/LICENSE +186 -186
  9. package/agent-spine-plugin/README.md +406 -0
  10. package/agent-spine-plugin/SECURITY.md +47 -0
  11. package/agent-spine-plugin/assets/agentspine-banner.svg +32 -32
  12. package/agent-spine-plugin/bin/agentspine-mcp.js +4 -4
  13. package/agent-spine-plugin/bin/agentspine.js +7 -7
  14. package/agent-spine-plugin/blun.plugin.json +33 -33
  15. package/agent-spine-plugin/hooks/codex.json +47 -47
  16. package/agent-spine-plugin/hooks/hooks.json +106 -106
  17. package/agent-spine-plugin/hooks/version.json +5 -5
  18. package/agent-spine-plugin/package.json +69 -69
  19. package/agent-spine-plugin/scripts/check-hosts.js +199 -199
  20. package/agent-spine-plugin/skill/SKILL.md +76 -76
  21. package/agent-spine-plugin/skills/agent-spine/SKILL.md +85 -85
  22. package/agent-spine-plugin/src/cli.js +1488 -1442
  23. package/agent-spine-plugin/src/hook.js +882 -861
  24. package/agent-spine-plugin/src/index.js +94 -93
  25. package/agent-spine-plugin/src/lib/acceptance.js +333 -333
  26. package/agent-spine-plugin/src/lib/attention.js +755 -755
  27. package/agent-spine-plugin/src/lib/audit.js +351 -342
  28. package/agent-spine-plugin/src/lib/authentication.js +515 -515
  29. package/agent-spine-plugin/src/lib/briefing.js +317 -317
  30. package/agent-spine-plugin/src/lib/catalog.js +167 -167
  31. package/agent-spine-plugin/src/lib/channel-runtime.js +665 -665
  32. package/agent-spine-plugin/src/lib/context.js +154 -154
  33. package/agent-spine-plugin/src/lib/continuity.js +338 -338
  34. package/agent-spine-plugin/src/lib/coordination.js +577 -577
  35. package/agent-spine-plugin/src/lib/documents.js +217 -217
  36. package/agent-spine-plugin/src/lib/feed-transport.js +499 -499
  37. package/agent-spine-plugin/src/lib/filesystem-retry.js +32 -32
  38. package/agent-spine-plugin/src/lib/gateway-runtime.js +1623 -760
  39. package/agent-spine-plugin/src/lib/graph.js +337 -337
  40. package/agent-spine-plugin/src/lib/hook-audit.js +29 -29
  41. package/agent-spine-plugin/src/lib/https-transport.js +392 -392
  42. package/agent-spine-plugin/src/lib/indexed-memory-offline.js +40 -40
  43. package/agent-spine-plugin/src/lib/indexed-memory.js +281 -281
  44. package/agent-spine-plugin/src/lib/learning.js +6923 -6466
  45. package/agent-spine-plugin/src/lib/object-transport.js +206 -206
  46. package/agent-spine-plugin/src/lib/owned-file-lock.js +143 -143
  47. package/agent-spine-plugin/src/lib/paths.js +109 -109
  48. package/agent-spine-plugin/src/lib/peer-transport.js +283 -283
  49. package/agent-spine-plugin/src/lib/persona-runtime.js +581 -581
  50. package/agent-spine-plugin/src/lib/preflight.js +702 -702
  51. package/agent-spine-plugin/src/lib/runtime.js +13 -13
  52. package/agent-spine-plugin/src/lib/selfstarter.js +911 -866
  53. package/agent-spine-plugin/src/lib/sharing.js +969 -969
  54. package/agent-spine-plugin/src/lib/source-roots.js +530 -498
  55. package/agent-spine-plugin/src/lib/sqlite-transport.js +501 -501
  56. package/agent-spine-plugin/src/lib/telegram-adapter.js +119 -119
  57. package/agent-spine-plugin/src/lib/voice-runtime.js +39 -39
  58. package/agent-spine-plugin/src/mcp.js +597 -572
  59. package/agent-spine-plugin/src/version.js +1 -1
  60. package/agent-spine-plugin/src/worker.js +210 -195
  61. package/bin/active-steer-priority-policy.cjs +24 -0
  62. package/bin/launcher-runtime.js +8 -1
  63. package/bin/mnemo-tool-agent-policy.cjs +22 -0
  64. package/bin/native-runtime-cache.cjs +76 -0
  65. package/bin/thinking-activity-status-policy.cjs +132 -0
  66. package/bin/thinking-only-guard.cjs +75 -0
  67. package/bin/tool-call-loop-policy.cjs +53 -0
  68. package/bin/turn-thinking-policy.cjs +25 -1
  69. package/blun.mjs +556 -63
  70. package/package.json +4 -1
  71. package/standard-skills/translate-native/LICENSE +21 -21
  72. package/standard-skills/translate-native/references/evaluation-protocol.md +95 -95
  73. package/standard-skills/translate-native/references/native-orthography.md +79 -79
  74. package/standard-skills/translate-native/references/native-translation-standard.md +94 -94
  75. package/standard-skills/translate-native/references/structured-content.md +72 -72
  76. package/standard-skills/translate-native/references/translationese-review.md +77 -77
  77. package/standard-skills/translate-native/scripts/blun_language_guard.py +697 -697
  78. package/standard-skills/translate-native/scripts/check_diacritics.py +353 -353
  79. package/standard-skills/translate-native/scripts/guard_service_client.py +264 -264
  80. package/standard-skills/translate-native/scripts/language_gateway.py +62 -62
  81. package/standard-skills/translate-native/scripts/language_quality.py +377 -377
  82. package/standard-skills/translate-native/scripts/pre_output_guard.py +64 -64
  83. package/standard-skills/translate-native/scripts/translation_guard.py +916 -916
  84. package/standard-tools/language-guard/blun_language_guard.py +697 -697
  85. package/standard-tools/language-guard/check_diacritics.py +353 -353
  86. package/standard-tools/language-guard/guard_service_client.py +264 -264
  87. package/standard-tools/language-guard/language_quality.py +377 -377
  88. package/standard-tools/language-guard/translation_guard.py +916 -916
@@ -1,697 +1,697 @@
1
- #!/usr/bin/env python3
2
- """BLUN Language Guard: zero-dependency CLI and MCP release gate."""
3
-
4
- from __future__ import annotations
5
-
6
- import argparse
7
- import importlib.util
8
- import json
9
- import os
10
- import re
11
- import sys
12
- import unicodedata
13
- from dataclasses import asdict, dataclass
14
- from pathlib import Path
15
- from typing import Any
16
-
17
-
18
- DIACRITICS_PATH = Path(__file__).with_name("check_diacritics.py")
19
- VERSION = "6.20.0"
20
- PROTOCOL_VERSION = "2025-06-18"
21
- SUPPORTED_PROTOCOL_VERSIONS = {"2025-03-26", PROTOCOL_VERSION}
22
- EXACT_LANGUAGE_TAG = re.compile(r"^(?:[A-Za-z]{2,8}|x)(?:-[A-Za-z0-9]{1,8})*$")
23
- MCP_INSTRUCTIONS = (
24
- "Treat every user-visible natural-language answer as an untrusted candidate. "
25
- "Before delivery, call release_response with the complete answer and exact language tag. "
26
- "For every translation, localization, transcreation, or target-language rewrite, first apply "
27
- "the installed translate-native skill/plugin and then call release_translation with the complete "
28
- "source-target pair and truthful seven-pass attestations. Never use release_response to bypass "
29
- "the translation gate. Release only after the exact current text receives a valid token. "
30
- "When BLUN_LANGUAGE_GUARD_MANDATORY=1, final stdout must be exactly one JSON object containing "
31
- "only target_text and release_token; never call a delivery channel directly or include host-owned policy fields."
32
- )
33
-
34
-
35
- def _load_diacritics_module():
36
- spec = importlib.util.spec_from_file_location("blun_check_diacritics", DIACRITICS_PATH)
37
- if spec is None or spec.loader is None:
38
- raise RuntimeError("Cannot load the bundled diacritics checker")
39
- module = importlib.util.module_from_spec(spec)
40
- spec.loader.exec_module(module)
41
- return module
42
-
43
-
44
- DIACRITICS = _load_diacritics_module()
45
-
46
-
47
- def _load_quality_module():
48
- path = Path(__file__).with_name("language_quality.py")
49
- spec = importlib.util.spec_from_file_location("blun_language_quality", path)
50
- if spec is None or spec.loader is None:
51
- raise RuntimeError("Cannot load language quality primitives")
52
- module = importlib.util.module_from_spec(spec)
53
- spec.loader.exec_module(module)
54
- return module
55
-
56
-
57
- QUALITY = _load_quality_module()
58
-
59
-
60
- def _load_translation_module():
61
- path = Path(__file__).with_name("translation_guard.py")
62
- spec = importlib.util.spec_from_file_location("blun_translation_guard", path)
63
- if spec is None or spec.loader is None:
64
- raise RuntimeError("Cannot load translation integrity primitives")
65
- module = importlib.util.module_from_spec(spec)
66
- spec.loader.exec_module(module)
67
- return module
68
-
69
-
70
- TRANSLATION = _load_translation_module()
71
-
72
-
73
- def _load_service_client():
74
- path = Path(__file__).with_name("guard_service_client.py")
75
- spec = importlib.util.spec_from_file_location("blun_guard_service_client", path)
76
- if spec is None or spec.loader is None:
77
- raise RuntimeError("Cannot load guard service client")
78
- module = importlib.util.module_from_spec(spec)
79
- spec.loader.exec_module(module)
80
- return module
81
-
82
-
83
- SERVICE_CLIENT = _load_service_client()
84
- VERSION = QUALITY.VERSION
85
- KEY_PATH = Path(os.environ.get("BLUN_LANGUAGE_GUARD_KEY_FILE", Path.home() / ".config" / "blun-language-guard" / "signing.key"))
86
- SERVICE_ENDPOINT = os.environ.get("BLUN_LANGUAGE_GUARD_SERVICE_ENDPOINT", "").strip()
87
- SERVICE_TOKEN_FILE = os.environ.get("BLUN_LANGUAGE_GUARD_SERVICE_TOKEN_FILE", "").strip()
88
- LANGUAGE_CHARACTER_PROFILES = {
89
- "sv": set("åäöÅÄÖ"),
90
- "de": set("äöüßÄÖÜẞ"),
91
- "es": set("áéíóúüñ¿¡ÁÉÍÓÚÜÑ"),
92
- "cs": set("áčďéěíňóřšťúůýžÁČĎÉĚÍŇÓŘŠŤÚŮÝŽ"),
93
- "ca": set("àçèéíïòóúü·ÀÇÈÉÍÏÒÓÚÜ"),
94
- }
95
- ASCII_FOLDING_PROFILES = {
96
- # Conventional transliterations whose density is measurable without a dictionary.
97
- # Thresholds deliberately avoid treating one ordinary letter sequence as proof.
98
- # German ``ue`` is also a native vowel sequence in words such as ``neue``
99
- # and ``Abenteuer`` and in the productive loan suffix ``-uell``. Count it
100
- # only where the surrounding letters still make an ASCII-folded umlaut
101
- # plausible. The short-stem exception deliberately keeps ``Muell`` and
102
- # similar folded forms measurable while accepting ``aktuell`` and
103
- # ``individuell``.
104
- "de": {
105
- "patterns": (
106
- r"ae",
107
- r"oe",
108
- r"(?<![aeiouyäöüq])(?:(?<![A-Za-zÄÖÜäöüß]{3})ue(?=ll)|ue(?!ll))",
109
- ),
110
- "native": "äöüÄÖÜ",
111
- "minimum": 3,
112
- },
113
- "sv": {"patterns": (r"aa", r"ae", r"oe"), "native": "åäöÅÄÖ", "minimum": 1},
114
- "da": {"patterns": (r"aa", r"ae", r"oe"), "native": "åæøÅÆØ", "minimum": 1},
115
- "no": {"patterns": (r"aa", r"ae", r"oe"), "native": "åæøÅÆØ", "minimum": 1},
116
- }
117
-
118
-
119
- @dataclass(frozen=True)
120
- class Finding:
121
- code: str
122
- message: str
123
- blocking: bool = True
124
- line: int | None = None
125
- language: str | None = None
126
-
127
-
128
- def _service_token() -> str:
129
- direct = os.environ.get("BLUN_LANGUAGE_GUARD_SERVICE_TOKEN", "").strip()
130
- if direct:
131
- return direct
132
- if not SERVICE_TOKEN_FILE:
133
- return ""
134
- return SERVICE_CLIENT.load_service_token(Path(SERVICE_TOKEN_FILE))
135
-
136
-
137
- def _isolated_release(task_kind: str, arguments: dict[str, Any]) -> dict[str, Any] | None:
138
- if not SERVICE_ENDPOINT:
139
- return None
140
- request = dict(arguments)
141
- request.update({
142
- "operation": "release",
143
- "task_kind": task_kind,
144
- "agent_id": os.environ.get("BLUN_LANGUAGE_GUARD_AGENT_ID", ""),
145
- "channel": os.environ.get("BLUN_LANGUAGE_GUARD_CHANNEL", "mcp"),
146
- })
147
- if task_kind == "response":
148
- request["source_text"] = ""
149
- try:
150
- return SERVICE_CLIENT.call_guard_service(
151
- SERVICE_ENDPOINT,
152
- request,
153
- auth_token=_service_token(),
154
- )
155
- except (OSError, SERVICE_CLIENT.GuardServiceError) as error:
156
- return {
157
- "status": "BLOCK",
158
- "release_allowed": False,
159
- "reason": "isolated-guard-unavailable",
160
- "error": str(error),
161
- }
162
-
163
-
164
- def _languages_for(text: str, language: str) -> tuple[str, ...]:
165
- if language == "all":
166
- return tuple(DIACRITICS.RULES)
167
- if language == "auto":
168
- return DIACRITICS.detect_languages(text)
169
- base = language.casefold().split("-", 1)[0].split("_", 1)[0]
170
- return (base,) if base in DIACRITICS.RULES else ()
171
-
172
-
173
- def validate_text(
174
- text: str,
175
- language: str = "auto",
176
- glossary: dict[str, Any] | None = None,
177
- content_type: str = "prose",
178
- short_text_reviewed: bool = False,
179
- ) -> dict[str, Any]:
180
- findings: list[Finding] = []
181
- if not text.strip():
182
- findings.append(Finding("empty-target", "Target text is empty."))
183
- if text != unicodedata.normalize("NFC", text):
184
- findings.append(Finding("unicode-not-nfc", "Target text is not NFC-normalized."))
185
- if "\ufffd" in text:
186
- findings.append(Finding("replacement-character", "Target contains U+FFFD replacement characters."))
187
- if "\x00" in text:
188
- findings.append(Finding("nul-character", "Target contains a NUL character."))
189
-
190
- for bidi in QUALITY.bidi_findings(text):
191
- findings.append(Finding(bidi["code"], json.dumps(bidi, ensure_ascii=False)))
192
-
193
- script = QUALITY.script_report(text, language)
194
- if script.get("status") == "fail":
195
- findings.append(Finding("script-mismatch", json.dumps(script, ensure_ascii=False), language=language))
196
- base_language = language.casefold().replace("_", "-").split("-", 1)[0]
197
- profile = LANGUAGE_CHARACTER_PROFILES.get(base_language)
198
- profile_prose = DIACRITICS.mask_technical_text(text)
199
- if profile and len(profile_prose) >= 200 and not any(character in profile for character in profile_prose):
200
- findings.append(Finding(
201
- "missing-language-character-profile",
202
- f"Long {base_language} text contains none of the language's characteristic native characters; possible wholesale ASCII folding.",
203
- language=language,
204
- ))
205
- folding_profile = ASCII_FOLDING_PROFILES.get(base_language)
206
- if folding_profile:
207
- folded = sum(len(re.findall(pattern, profile_prose, re.IGNORECASE)) for pattern in folding_profile["patterns"])
208
- native = sum(profile_prose.count(character) for character in folding_profile["native"])
209
- if folded >= folding_profile["minimum"] and folded > native:
210
- findings.append(Finding(
211
- "ascii-folding-pressure",
212
- f"Measured ASCII-folding candidates ({folded}) exceed native characters ({native}); review the exact spelling.",
213
- language=language,
214
- ))
215
- # Kept as compatibility metadata only. It never suppresses a measurable finding.
216
- short_sensitive = content_type in {"title", "meta_description", "ui"} and len(profile_prose.strip()) < 200
217
- if short_sensitive and not short_text_reviewed and not findings:
218
- findings.append(Finding(
219
- "short-text-native-review-required",
220
- f"Short {content_type} text needs host-enforced review; an MCP Boolean is not independent proof.",
221
- language=language,
222
- ))
223
- for glossary_finding in QUALITY.glossary_findings(
224
- text, glossary if isinstance(glossary, dict) else {}
225
- ):
226
- findings.append(Finding(glossary_finding["code"], json.dumps(glossary_finding, ensure_ascii=False), language=language))
227
-
228
- prose = DIACRITICS.mask_technical_text(text)
229
- for line, code, found, suggestion in DIACRITICS.iter_findings(
230
- prose, _languages_for(prose, language)
231
- ):
232
- findings.append(
233
- Finding(
234
- "suspected-ascii-substitution",
235
- f"{found!r} may require native spelling {suggestion!r}.",
236
- line=line,
237
- language=code,
238
- )
239
- )
240
-
241
- return {
242
- "status": (
243
- "REVIEW_REQUIRED"
244
- if findings and all(finding.code == "short-text-native-review-required" for finding in findings)
245
- else "BLOCK" if findings else "PASS"
246
- ),
247
- "release_allowed": not findings,
248
- "language": language,
249
- "checks": [
250
- "non-empty",
251
- "unicode-nfc",
252
- "encoding-integrity",
253
- "bidi-control-safety",
254
- "native-diacritics-heuristics",
255
- ],
256
- "findings": [asdict(finding) for finding in findings],
257
- "limitations": (
258
- "Deterministic checks cannot prove semantic fidelity or native fluency. "
259
- "The release gate therefore also requires explicit seven-pass attestations."
260
- ),
261
- }
262
-
263
-
264
- def release_translation(arguments: dict[str, Any]) -> dict[str, Any]:
265
- isolated = _isolated_release("translation", arguments)
266
- if isolated is not None:
267
- return isolated
268
- source = arguments.get("source_text", "")
269
- target = arguments.get("target_text", "")
270
- language = arguments.get("language", "auto")
271
- source_is_text = isinstance(source, str)
272
- target_is_text = isinstance(target, str)
273
- language_is_exact = (
274
- isinstance(language, str)
275
- and language.casefold() not in {"auto", "all"}
276
- and EXACT_LANGUAGE_TAG.fullmatch(language) is not None
277
- )
278
- source = source if source_is_text else ""
279
- target = target if target_is_text else ""
280
- language = language if isinstance(language, str) else ""
281
- attestations = arguments.get("attestations") or {}
282
- if not isinstance(attestations, dict):
283
- attestations = {}
284
- required = (
285
- "meaning",
286
- "completeness",
287
- "precision",
288
- "nativeness",
289
- "locale_fit",
290
- "integrity",
291
- "orthography",
292
- )
293
- report = validate_text(
294
- target,
295
- language,
296
- arguments.get("glossary"),
297
- arguments.get("content_type", "prose"),
298
- arguments.get("short_text_reviewed") is True,
299
- )
300
- report["checks"].extend([
301
- "source-target-identity",
302
- "structured-segment-identity",
303
- "translation-volume-integrity",
304
- ])
305
- if not source_is_text:
306
- report["findings"].append(
307
- asdict(Finding("invalid-source-type", "source_text must be a string."))
308
- )
309
- if not target_is_text:
310
- report["findings"].append(
311
- asdict(Finding("invalid-target-type", "target_text must be a string."))
312
- )
313
- if not language_is_exact:
314
- report["findings"].append(
315
- asdict(Finding(
316
- "exact-language-required",
317
- "A host-supplied exact language or locale tag is required for translation release.",
318
- ))
319
- )
320
- missing = [name for name in required if attestations.get(name) is not True]
321
- if not source.strip():
322
- report["findings"].append(
323
- asdict(Finding("empty-source", "Source text is required for the fidelity gate."))
324
- )
325
- else:
326
- whole_identity_errors = TRANSLATION.identity_errors(source, target)
327
- for error in whole_identity_errors:
328
- report["findings"].append(
329
- asdict(Finding("source-target-identical", error))
330
- )
331
- if not whole_identity_errors:
332
- selected_format = TRANSLATION.detect_content_format(source)
333
- for error in TRANSLATION.structured_identity_errors(
334
- source, target, selected_format
335
- ):
336
- report["findings"].append(
337
- asdict(Finding("unchanged-linguistic-segment", error))
338
- )
339
- for error in TRANSLATION.translation_volume_errors(source, target):
340
- report["findings"].append(
341
- asdict(Finding("translation-volume-integrity", error))
342
- )
343
- if missing:
344
- report["findings"].append(
345
- asdict(
346
- Finding(
347
- "missing-attestations",
348
- "The following release checks were not explicitly passed: "
349
- + ", ".join(missing),
350
- )
351
- )
352
- )
353
- review_only = report["findings"] and all(
354
- finding.get("code") == "short-text-native-review-required" for finding in report["findings"]
355
- )
356
- report["status"] = "REVIEW_REQUIRED" if review_only else "BLOCK" if report["findings"] else "PASS"
357
- report["release_allowed"] = not report["findings"]
358
- report["required_attestations"] = list(required)
359
- if report["release_allowed"]:
360
- key = QUALITY.load_or_create_key(KEY_PATH)
361
- report["release_token"] = QUALITY.issue_receipt(
362
- source, target, language, key,
363
- content_type=arguments.get("content_type", "prose"),
364
- short_text_reviewed=arguments.get("short_text_reviewed") is True,
365
- purpose="translation",
366
- )
367
- return report
368
-
369
-
370
- def release_response(arguments: dict[str, Any]) -> dict[str, Any]:
371
- """Validate an agent's own final answer and bind a receipt to the exact text."""
372
- isolated = _isolated_release("response", arguments)
373
- if isolated is not None:
374
- return isolated
375
- target = arguments.get("target_text", "")
376
- language = arguments.get("language", "")
377
- attestations = arguments.get("attestations") or {}
378
- if not isinstance(attestations, dict):
379
- attestations = {}
380
- target_is_text = isinstance(target, str)
381
- language_is_exact = (
382
- isinstance(language, str)
383
- and language.casefold() not in {"auto", "all"}
384
- and EXACT_LANGUAGE_TAG.fullmatch(language) is not None
385
- )
386
- report = validate_text(
387
- target if target_is_text else "",
388
- language if isinstance(language, str) else "",
389
- arguments.get("glossary"),
390
- arguments.get("content_type", "prose"),
391
- arguments.get("short_text_reviewed") is True,
392
- )
393
- report["checks"].append("agent-response-native-orthography")
394
- if not target_is_text:
395
- report["findings"].append(
396
- asdict(Finding("invalid-target-type", "target_text must be a string."))
397
- )
398
- if not language_is_exact:
399
- report["findings"].append(
400
- asdict(Finding(
401
- "exact-language-required",
402
- "A host-supplied exact language or locale tag is required for response release.",
403
- ))
404
- )
405
- missing = [name for name in ("nativeness", "orthography") if attestations.get(name) is not True]
406
- if missing:
407
- report["findings"].append(
408
- asdict(Finding(
409
- "missing-response-attestations",
410
- "The following response checks were not explicitly passed: " + ", ".join(missing),
411
- ))
412
- )
413
- report["status"] = "BLOCK" if report["findings"] else "PASS"
414
- report["release_allowed"] = not report["findings"]
415
- report["required_attestations"] = ["nativeness", "orthography"]
416
- report["limitations"] = (
417
- "Deterministic checks cannot prove that every word is native or correctly accented. "
418
- "Response release also requires nativeness and orthography review plus a trusted host interceptor."
419
- )
420
- if report["release_allowed"]:
421
- key = QUALITY.load_or_create_key(KEY_PATH)
422
- report["release_token"] = QUALITY.issue_receipt(
423
- "", target, language, key,
424
- content_type=arguments.get("content_type", "prose"),
425
- short_text_reviewed=arguments.get("short_text_reviewed") is True,
426
- purpose="response",
427
- )
428
- return report
429
-
430
-
431
- TOOLS = [
432
- {
433
- "name": "verify_release_token",
434
- "description": "Cryptographically verify that a BLUN release receipt is authentic, unexpired, and bound to the exact purpose, source when applicable, target, locale, and guard version. Never accept a receipt based on its appearance.",
435
- "inputSchema": {
436
- "type": "object",
437
- "properties": {
438
- "release_token": {"type": "string"},
439
- "source_text": {"type": "string"},
440
- "target_text": {"type": "string"},
441
- "language": {"type": "string"},
442
- "purpose": {"type": "string", "enum": ["translation", "response"], "default": "translation"},
443
- "content_type": {"type": "string", "enum": ["prose", "title", "meta_description", "ui"], "default": "prose"},
444
- "short_text_reviewed": {"type": "boolean", "default": False},
445
- },
446
- "required": ["release_token", "source_text", "target_text", "language"],
447
- "additionalProperties": False,
448
- },
449
- },
450
- {
451
- "name": "release_response",
452
- "description": "Mandatory final gate for an agent's own user-visible natural-language answer. Returns a purpose-bound token only after deterministic Unicode, script, native-diacritics, and explicit nativeness/orthography checks pass. Never use this tool for a translation.",
453
- "inputSchema": {
454
- "type": "object",
455
- "properties": {
456
- "target_text": {"type": "string"},
457
- "language": {"type": "string", "description": "Exact BCP 47 language or locale tag supplied by the host; auto and all are rejected."},
458
- "glossary": {"type": "object"},
459
- "content_type": {"type": "string", "enum": ["prose", "title", "meta_description", "ui"], "default": "prose"},
460
- "short_text_reviewed": {"type": "boolean", "description": "Compatibility metadata only; never suppresses measurable findings."},
461
- "attestations": {
462
- "type": "object",
463
- "properties": {
464
- "nativeness": {"type": "boolean"},
465
- "orthography": {"type": "boolean"},
466
- },
467
- "required": ["nativeness", "orthography"],
468
- "additionalProperties": False,
469
- },
470
- },
471
- "required": ["target_text", "language", "attestations"],
472
- "additionalProperties": False,
473
- },
474
- },
475
- {
476
- "name": "validate_text",
477
- "description": "Run deterministic Unicode, script-safety, and native-diacritics checks on target-language text.",
478
- "inputSchema": {
479
- "type": "object",
480
- "properties": {
481
- "text": {"type": "string"},
482
- "language": {"type": "string", "default": "auto"},
483
- "glossary": {"type": "object", "description": "Optional source-term to required target-term or regex-rule map."},
484
- "content_type": {"type": "string", "enum": ["prose", "title", "meta_description", "ui"], "default": "prose"},
485
- "short_text_reviewed": {"type": "boolean", "default": False},
486
- },
487
- "required": ["text"],
488
- "additionalProperties": False,
489
- },
490
- },
491
- {
492
- "name": "release_translation",
493
- "description": "Mandatory final gate. Returns a release token only after deterministic validation, whole-input and structured-segment source-target non-identity, auto-detected translation-volume integrity, and all seven quality attestations pass.",
494
- "inputSchema": {
495
- "type": "object",
496
- "properties": {
497
- "source_text": {"type": "string"},
498
- "target_text": {"type": "string"},
499
- "language": {"type": "string"},
500
- "content_type": {"type": "string", "enum": ["prose", "title", "meta_description", "ui"], "default": "prose"},
501
- "short_text_reviewed": {"type": "boolean", "description": "Compatibility metadata only. Never suppresses measurable findings and is not independent proof."},
502
- "attestations": {
503
- "type": "object",
504
- "properties": {
505
- name: {"type": "boolean"}
506
- for name in (
507
- "meaning",
508
- "completeness",
509
- "precision",
510
- "nativeness",
511
- "locale_fit",
512
- "integrity",
513
- "orthography",
514
- )
515
- },
516
- "required": [
517
- "meaning",
518
- "completeness",
519
- "precision",
520
- "nativeness",
521
- "locale_fit",
522
- "integrity",
523
- "orthography",
524
- ],
525
- "additionalProperties": False,
526
- },
527
- },
528
- "required": ["source_text", "target_text", "language", "attestations"],
529
- "additionalProperties": False,
530
- },
531
- },
532
- ]
533
-
534
-
535
- def _tool_result(payload: dict[str, Any]) -> dict[str, Any]:
536
- return {
537
- "content": [{"type": "text", "text": json.dumps(payload, ensure_ascii=False)}],
538
- "structuredContent": payload,
539
- "isError": payload.get("status") != "PASS",
540
- }
541
-
542
-
543
- def handle_message(message: dict[str, Any]) -> dict[str, Any] | None:
544
- method = message.get("method")
545
- request_id = message.get("id")
546
- if request_id is None:
547
- return None
548
- if method == "initialize":
549
- params = message.get("params") if isinstance(message.get("params"), dict) else {}
550
- requested_protocol = params.get("protocolVersion")
551
- negotiated_protocol = (
552
- requested_protocol
553
- if isinstance(requested_protocol, str) and requested_protocol in SUPPORTED_PROTOCOL_VERSIONS
554
- else PROTOCOL_VERSION
555
- )
556
- return {
557
- "jsonrpc": "2.0",
558
- "id": request_id,
559
- "result": {
560
- "protocolVersion": negotiated_protocol,
561
- "capabilities": {
562
- "tools": {"listChanged": False},
563
- "prompts": {"listChanged": False},
564
- },
565
- "serverInfo": {"name": "blun-language-guard", "version": VERSION},
566
- "instructions": MCP_INSTRUCTIONS,
567
- },
568
- }
569
- if method == "ping":
570
- return {"jsonrpc": "2.0", "id": request_id, "result": {}}
571
- if method == "tools/list":
572
- return {"jsonrpc": "2.0", "id": request_id, "result": {"tools": TOOLS}}
573
- if method == "prompts/list":
574
- return {
575
- "jsonrpc": "2.0",
576
- "id": request_id,
577
- "result": {"prompts": [{
578
- "name": "translate-native",
579
- "title": "Translate Native mandatory workflow",
580
- "description": "Load the native translation workflow before drafting any translation.",
581
- "arguments": [],
582
- }]},
583
- }
584
- if method == "prompts/get":
585
- params = message.get("params") or {}
586
- if params.get("name") != "translate-native":
587
- return {
588
- "jsonrpc": "2.0",
589
- "id": request_id,
590
- "error": {"code": -32602, "message": "Unknown prompt"},
591
- }
592
- return {
593
- "jsonrpc": "2.0",
594
- "id": request_id,
595
- "result": {
596
- "description": "Mandatory native translation and orthography workflow.",
597
- "messages": [{
598
- "role": "user",
599
- "content": {"type": "text", "text": MCP_INSTRUCTIONS},
600
- }],
601
- },
602
- }
603
- if method == "tools/call":
604
- params = message.get("params") or {}
605
- name = params.get("name")
606
- arguments = params.get("arguments") or {}
607
- if name == "validate_text":
608
- payload = validate_text(
609
- arguments.get("text", ""), arguments.get("language", "auto"), arguments.get("glossary"),
610
- arguments.get("content_type", "prose"), arguments.get("short_text_reviewed") is True,
611
- )
612
- elif name == "release_translation":
613
- payload = release_translation(arguments)
614
- elif name == "release_response":
615
- payload = release_response(arguments)
616
- elif name == "verify_release_token":
617
- if SERVICE_ENDPOINT:
618
- try:
619
- payload = SERVICE_CLIENT.call_guard_service(
620
- SERVICE_ENDPOINT,
621
- {
622
- "operation": "verify",
623
- "task_kind": arguments.get("purpose", "translation"),
624
- "source_text": arguments.get("source_text", ""),
625
- "target_text": arguments.get("target_text", ""),
626
- "language": arguments.get("language", ""),
627
- "release_token": arguments.get("release_token", ""),
628
- "content_type": arguments.get("content_type", "prose"),
629
- "short_text_reviewed": arguments.get("short_text_reviewed") is True,
630
- "agent_id": os.environ.get("BLUN_LANGUAGE_GUARD_AGENT_ID", ""),
631
- "channel": os.environ.get("BLUN_LANGUAGE_GUARD_CHANNEL", "mcp"),
632
- },
633
- auth_token=_service_token(),
634
- )
635
- except (OSError, SERVICE_CLIENT.GuardServiceError) as error:
636
- payload = {"valid": False, "status": "BLOCK", "error": str(error)}
637
- else:
638
- payload = QUALITY.verify_receipt(
639
- arguments.get("release_token", ""),
640
- arguments.get("source_text", ""),
641
- arguments.get("target_text", ""),
642
- arguments.get("language", ""),
643
- QUALITY.load_or_create_key(KEY_PATH),
644
- arguments.get("content_type", "prose"),
645
- arguments.get("short_text_reviewed") is True,
646
- arguments.get("purpose", "translation"),
647
- )
648
- payload["status"] = "PASS" if payload.get("valid") else "BLOCK"
649
- else:
650
- return {
651
- "jsonrpc": "2.0",
652
- "id": request_id,
653
- "error": {"code": -32601, "message": f"Unknown tool: {name}"},
654
- }
655
- return {"jsonrpc": "2.0", "id": request_id, "result": _tool_result(payload)}
656
- return {
657
- "jsonrpc": "2.0",
658
- "id": request_id,
659
- "error": {"code": -32601, "message": f"Unknown method: {method}"},
660
- }
661
-
662
-
663
- def serve() -> int:
664
- for line in sys.stdin:
665
- if not line.strip():
666
- continue
667
- try:
668
- response = handle_message(json.loads(line.lstrip("\ufeff")))
669
- except Exception as error: # Keep the MCP process alive after malformed input.
670
- response = {
671
- "jsonrpc": "2.0",
672
- "id": None,
673
- "error": {"code": -32603, "message": str(error)},
674
- }
675
- if response is not None:
676
- print(json.dumps(response, ensure_ascii=False), flush=True)
677
- return 0
678
-
679
-
680
- def main() -> int:
681
- parser = argparse.ArgumentParser(description="BLUN Language Guard")
682
- subparsers = parser.add_subparsers(dest="command", required=True)
683
- subparsers.add_parser("serve", help="Run the MCP server over stdio")
684
- validate = subparsers.add_parser("validate", help="Validate text from a file or stdin")
685
- validate.add_argument("path", nargs="?", type=Path)
686
- validate.add_argument("--language", default="auto")
687
- args = parser.parse_args()
688
- if args.command == "serve":
689
- return serve()
690
- text = args.path.read_text(encoding="utf-8") if args.path else sys.stdin.read()
691
- report = validate_text(text, args.language)
692
- print(json.dumps(report, ensure_ascii=False, indent=2))
693
- return 0 if report["release_allowed"] else 1
694
-
695
-
696
- if __name__ == "__main__":
697
- raise SystemExit(main())
1
+ #!/usr/bin/env python3
2
+ """BLUN Language Guard: zero-dependency CLI and MCP release gate."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import importlib.util
8
+ import json
9
+ import os
10
+ import re
11
+ import sys
12
+ import unicodedata
13
+ from dataclasses import asdict, dataclass
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+
18
+ DIACRITICS_PATH = Path(__file__).with_name("check_diacritics.py")
19
+ VERSION = "6.20.0"
20
+ PROTOCOL_VERSION = "2025-06-18"
21
+ SUPPORTED_PROTOCOL_VERSIONS = {"2025-03-26", PROTOCOL_VERSION}
22
+ EXACT_LANGUAGE_TAG = re.compile(r"^(?:[A-Za-z]{2,8}|x)(?:-[A-Za-z0-9]{1,8})*$")
23
+ MCP_INSTRUCTIONS = (
24
+ "Treat every user-visible natural-language answer as an untrusted candidate. "
25
+ "Before delivery, call release_response with the complete answer and exact language tag. "
26
+ "For every translation, localization, transcreation, or target-language rewrite, first apply "
27
+ "the installed translate-native skill/plugin and then call release_translation with the complete "
28
+ "source-target pair and truthful seven-pass attestations. Never use release_response to bypass "
29
+ "the translation gate. Release only after the exact current text receives a valid token. "
30
+ "When BLUN_LANGUAGE_GUARD_MANDATORY=1, final stdout must be exactly one JSON object containing "
31
+ "only target_text and release_token; never call a delivery channel directly or include host-owned policy fields."
32
+ )
33
+
34
+
35
+ def _load_diacritics_module():
36
+ spec = importlib.util.spec_from_file_location("blun_check_diacritics", DIACRITICS_PATH)
37
+ if spec is None or spec.loader is None:
38
+ raise RuntimeError("Cannot load the bundled diacritics checker")
39
+ module = importlib.util.module_from_spec(spec)
40
+ spec.loader.exec_module(module)
41
+ return module
42
+
43
+
44
+ DIACRITICS = _load_diacritics_module()
45
+
46
+
47
+ def _load_quality_module():
48
+ path = Path(__file__).with_name("language_quality.py")
49
+ spec = importlib.util.spec_from_file_location("blun_language_quality", path)
50
+ if spec is None or spec.loader is None:
51
+ raise RuntimeError("Cannot load language quality primitives")
52
+ module = importlib.util.module_from_spec(spec)
53
+ spec.loader.exec_module(module)
54
+ return module
55
+
56
+
57
+ QUALITY = _load_quality_module()
58
+
59
+
60
+ def _load_translation_module():
61
+ path = Path(__file__).with_name("translation_guard.py")
62
+ spec = importlib.util.spec_from_file_location("blun_translation_guard", path)
63
+ if spec is None or spec.loader is None:
64
+ raise RuntimeError("Cannot load translation integrity primitives")
65
+ module = importlib.util.module_from_spec(spec)
66
+ spec.loader.exec_module(module)
67
+ return module
68
+
69
+
70
+ TRANSLATION = _load_translation_module()
71
+
72
+
73
+ def _load_service_client():
74
+ path = Path(__file__).with_name("guard_service_client.py")
75
+ spec = importlib.util.spec_from_file_location("blun_guard_service_client", path)
76
+ if spec is None or spec.loader is None:
77
+ raise RuntimeError("Cannot load guard service client")
78
+ module = importlib.util.module_from_spec(spec)
79
+ spec.loader.exec_module(module)
80
+ return module
81
+
82
+
83
+ SERVICE_CLIENT = _load_service_client()
84
+ VERSION = QUALITY.VERSION
85
+ KEY_PATH = Path(os.environ.get("BLUN_LANGUAGE_GUARD_KEY_FILE", Path.home() / ".config" / "blun-language-guard" / "signing.key"))
86
+ SERVICE_ENDPOINT = os.environ.get("BLUN_LANGUAGE_GUARD_SERVICE_ENDPOINT", "").strip()
87
+ SERVICE_TOKEN_FILE = os.environ.get("BLUN_LANGUAGE_GUARD_SERVICE_TOKEN_FILE", "").strip()
88
+ LANGUAGE_CHARACTER_PROFILES = {
89
+ "sv": set("åäöÅÄÖ"),
90
+ "de": set("äöüßÄÖÜẞ"),
91
+ "es": set("áéíóúüñ¿¡ÁÉÍÓÚÜÑ"),
92
+ "cs": set("áčďéěíňóřšťúůýžÁČĎÉĚÍŇÓŘŠŤÚŮÝŽ"),
93
+ "ca": set("àçèéíïòóúü·ÀÇÈÉÍÏÒÓÚÜ"),
94
+ }
95
+ ASCII_FOLDING_PROFILES = {
96
+ # Conventional transliterations whose density is measurable without a dictionary.
97
+ # Thresholds deliberately avoid treating one ordinary letter sequence as proof.
98
+ # German ``ue`` is also a native vowel sequence in words such as ``neue``
99
+ # and ``Abenteuer`` and in the productive loan suffix ``-uell``. Count it
100
+ # only where the surrounding letters still make an ASCII-folded umlaut
101
+ # plausible. The short-stem exception deliberately keeps ``Muell`` and
102
+ # similar folded forms measurable while accepting ``aktuell`` and
103
+ # ``individuell``.
104
+ "de": {
105
+ "patterns": (
106
+ r"ae",
107
+ r"oe",
108
+ r"(?<![aeiouyäöüq])(?:(?<![A-Za-zÄÖÜäöüß]{3})ue(?=ll)|ue(?!ll))",
109
+ ),
110
+ "native": "äöüÄÖÜ",
111
+ "minimum": 3,
112
+ },
113
+ "sv": {"patterns": (r"aa", r"ae", r"oe"), "native": "åäöÅÄÖ", "minimum": 1},
114
+ "da": {"patterns": (r"aa", r"ae", r"oe"), "native": "åæøÅÆØ", "minimum": 1},
115
+ "no": {"patterns": (r"aa", r"ae", r"oe"), "native": "åæøÅÆØ", "minimum": 1},
116
+ }
117
+
118
+
119
+ @dataclass(frozen=True)
120
+ class Finding:
121
+ code: str
122
+ message: str
123
+ blocking: bool = True
124
+ line: int | None = None
125
+ language: str | None = None
126
+
127
+
128
+ def _service_token() -> str:
129
+ direct = os.environ.get("BLUN_LANGUAGE_GUARD_SERVICE_TOKEN", "").strip()
130
+ if direct:
131
+ return direct
132
+ if not SERVICE_TOKEN_FILE:
133
+ return ""
134
+ return SERVICE_CLIENT.load_service_token(Path(SERVICE_TOKEN_FILE))
135
+
136
+
137
+ def _isolated_release(task_kind: str, arguments: dict[str, Any]) -> dict[str, Any] | None:
138
+ if not SERVICE_ENDPOINT:
139
+ return None
140
+ request = dict(arguments)
141
+ request.update({
142
+ "operation": "release",
143
+ "task_kind": task_kind,
144
+ "agent_id": os.environ.get("BLUN_LANGUAGE_GUARD_AGENT_ID", ""),
145
+ "channel": os.environ.get("BLUN_LANGUAGE_GUARD_CHANNEL", "mcp"),
146
+ })
147
+ if task_kind == "response":
148
+ request["source_text"] = ""
149
+ try:
150
+ return SERVICE_CLIENT.call_guard_service(
151
+ SERVICE_ENDPOINT,
152
+ request,
153
+ auth_token=_service_token(),
154
+ )
155
+ except (OSError, SERVICE_CLIENT.GuardServiceError) as error:
156
+ return {
157
+ "status": "BLOCK",
158
+ "release_allowed": False,
159
+ "reason": "isolated-guard-unavailable",
160
+ "error": str(error),
161
+ }
162
+
163
+
164
+ def _languages_for(text: str, language: str) -> tuple[str, ...]:
165
+ if language == "all":
166
+ return tuple(DIACRITICS.RULES)
167
+ if language == "auto":
168
+ return DIACRITICS.detect_languages(text)
169
+ base = language.casefold().split("-", 1)[0].split("_", 1)[0]
170
+ return (base,) if base in DIACRITICS.RULES else ()
171
+
172
+
173
+ def validate_text(
174
+ text: str,
175
+ language: str = "auto",
176
+ glossary: dict[str, Any] | None = None,
177
+ content_type: str = "prose",
178
+ short_text_reviewed: bool = False,
179
+ ) -> dict[str, Any]:
180
+ findings: list[Finding] = []
181
+ if not text.strip():
182
+ findings.append(Finding("empty-target", "Target text is empty."))
183
+ if text != unicodedata.normalize("NFC", text):
184
+ findings.append(Finding("unicode-not-nfc", "Target text is not NFC-normalized."))
185
+ if "\ufffd" in text:
186
+ findings.append(Finding("replacement-character", "Target contains U+FFFD replacement characters."))
187
+ if "\x00" in text:
188
+ findings.append(Finding("nul-character", "Target contains a NUL character."))
189
+
190
+ for bidi in QUALITY.bidi_findings(text):
191
+ findings.append(Finding(bidi["code"], json.dumps(bidi, ensure_ascii=False)))
192
+
193
+ script = QUALITY.script_report(text, language)
194
+ if script.get("status") == "fail":
195
+ findings.append(Finding("script-mismatch", json.dumps(script, ensure_ascii=False), language=language))
196
+ base_language = language.casefold().replace("_", "-").split("-", 1)[0]
197
+ profile = LANGUAGE_CHARACTER_PROFILES.get(base_language)
198
+ profile_prose = DIACRITICS.mask_technical_text(text)
199
+ if profile and len(profile_prose) >= 200 and not any(character in profile for character in profile_prose):
200
+ findings.append(Finding(
201
+ "missing-language-character-profile",
202
+ f"Long {base_language} text contains none of the language's characteristic native characters; possible wholesale ASCII folding.",
203
+ language=language,
204
+ ))
205
+ folding_profile = ASCII_FOLDING_PROFILES.get(base_language)
206
+ if folding_profile:
207
+ folded = sum(len(re.findall(pattern, profile_prose, re.IGNORECASE)) for pattern in folding_profile["patterns"])
208
+ native = sum(profile_prose.count(character) for character in folding_profile["native"])
209
+ if folded >= folding_profile["minimum"] and folded > native:
210
+ findings.append(Finding(
211
+ "ascii-folding-pressure",
212
+ f"Measured ASCII-folding candidates ({folded}) exceed native characters ({native}); review the exact spelling.",
213
+ language=language,
214
+ ))
215
+ # Kept as compatibility metadata only. It never suppresses a measurable finding.
216
+ short_sensitive = content_type in {"title", "meta_description", "ui"} and len(profile_prose.strip()) < 200
217
+ if short_sensitive and not short_text_reviewed and not findings:
218
+ findings.append(Finding(
219
+ "short-text-native-review-required",
220
+ f"Short {content_type} text needs host-enforced review; an MCP Boolean is not independent proof.",
221
+ language=language,
222
+ ))
223
+ for glossary_finding in QUALITY.glossary_findings(
224
+ text, glossary if isinstance(glossary, dict) else {}
225
+ ):
226
+ findings.append(Finding(glossary_finding["code"], json.dumps(glossary_finding, ensure_ascii=False), language=language))
227
+
228
+ prose = DIACRITICS.mask_technical_text(text)
229
+ for line, code, found, suggestion in DIACRITICS.iter_findings(
230
+ prose, _languages_for(prose, language)
231
+ ):
232
+ findings.append(
233
+ Finding(
234
+ "suspected-ascii-substitution",
235
+ f"{found!r} may require native spelling {suggestion!r}.",
236
+ line=line,
237
+ language=code,
238
+ )
239
+ )
240
+
241
+ return {
242
+ "status": (
243
+ "REVIEW_REQUIRED"
244
+ if findings and all(finding.code == "short-text-native-review-required" for finding in findings)
245
+ else "BLOCK" if findings else "PASS"
246
+ ),
247
+ "release_allowed": not findings,
248
+ "language": language,
249
+ "checks": [
250
+ "non-empty",
251
+ "unicode-nfc",
252
+ "encoding-integrity",
253
+ "bidi-control-safety",
254
+ "native-diacritics-heuristics",
255
+ ],
256
+ "findings": [asdict(finding) for finding in findings],
257
+ "limitations": (
258
+ "Deterministic checks cannot prove semantic fidelity or native fluency. "
259
+ "The release gate therefore also requires explicit seven-pass attestations."
260
+ ),
261
+ }
262
+
263
+
264
+ def release_translation(arguments: dict[str, Any]) -> dict[str, Any]:
265
+ isolated = _isolated_release("translation", arguments)
266
+ if isolated is not None:
267
+ return isolated
268
+ source = arguments.get("source_text", "")
269
+ target = arguments.get("target_text", "")
270
+ language = arguments.get("language", "auto")
271
+ source_is_text = isinstance(source, str)
272
+ target_is_text = isinstance(target, str)
273
+ language_is_exact = (
274
+ isinstance(language, str)
275
+ and language.casefold() not in {"auto", "all"}
276
+ and EXACT_LANGUAGE_TAG.fullmatch(language) is not None
277
+ )
278
+ source = source if source_is_text else ""
279
+ target = target if target_is_text else ""
280
+ language = language if isinstance(language, str) else ""
281
+ attestations = arguments.get("attestations") or {}
282
+ if not isinstance(attestations, dict):
283
+ attestations = {}
284
+ required = (
285
+ "meaning",
286
+ "completeness",
287
+ "precision",
288
+ "nativeness",
289
+ "locale_fit",
290
+ "integrity",
291
+ "orthography",
292
+ )
293
+ report = validate_text(
294
+ target,
295
+ language,
296
+ arguments.get("glossary"),
297
+ arguments.get("content_type", "prose"),
298
+ arguments.get("short_text_reviewed") is True,
299
+ )
300
+ report["checks"].extend([
301
+ "source-target-identity",
302
+ "structured-segment-identity",
303
+ "translation-volume-integrity",
304
+ ])
305
+ if not source_is_text:
306
+ report["findings"].append(
307
+ asdict(Finding("invalid-source-type", "source_text must be a string."))
308
+ )
309
+ if not target_is_text:
310
+ report["findings"].append(
311
+ asdict(Finding("invalid-target-type", "target_text must be a string."))
312
+ )
313
+ if not language_is_exact:
314
+ report["findings"].append(
315
+ asdict(Finding(
316
+ "exact-language-required",
317
+ "A host-supplied exact language or locale tag is required for translation release.",
318
+ ))
319
+ )
320
+ missing = [name for name in required if attestations.get(name) is not True]
321
+ if not source.strip():
322
+ report["findings"].append(
323
+ asdict(Finding("empty-source", "Source text is required for the fidelity gate."))
324
+ )
325
+ else:
326
+ whole_identity_errors = TRANSLATION.identity_errors(source, target)
327
+ for error in whole_identity_errors:
328
+ report["findings"].append(
329
+ asdict(Finding("source-target-identical", error))
330
+ )
331
+ if not whole_identity_errors:
332
+ selected_format = TRANSLATION.detect_content_format(source)
333
+ for error in TRANSLATION.structured_identity_errors(
334
+ source, target, selected_format
335
+ ):
336
+ report["findings"].append(
337
+ asdict(Finding("unchanged-linguistic-segment", error))
338
+ )
339
+ for error in TRANSLATION.translation_volume_errors(source, target):
340
+ report["findings"].append(
341
+ asdict(Finding("translation-volume-integrity", error))
342
+ )
343
+ if missing:
344
+ report["findings"].append(
345
+ asdict(
346
+ Finding(
347
+ "missing-attestations",
348
+ "The following release checks were not explicitly passed: "
349
+ + ", ".join(missing),
350
+ )
351
+ )
352
+ )
353
+ review_only = report["findings"] and all(
354
+ finding.get("code") == "short-text-native-review-required" for finding in report["findings"]
355
+ )
356
+ report["status"] = "REVIEW_REQUIRED" if review_only else "BLOCK" if report["findings"] else "PASS"
357
+ report["release_allowed"] = not report["findings"]
358
+ report["required_attestations"] = list(required)
359
+ if report["release_allowed"]:
360
+ key = QUALITY.load_or_create_key(KEY_PATH)
361
+ report["release_token"] = QUALITY.issue_receipt(
362
+ source, target, language, key,
363
+ content_type=arguments.get("content_type", "prose"),
364
+ short_text_reviewed=arguments.get("short_text_reviewed") is True,
365
+ purpose="translation",
366
+ )
367
+ return report
368
+
369
+
370
+ def release_response(arguments: dict[str, Any]) -> dict[str, Any]:
371
+ """Validate an agent's own final answer and bind a receipt to the exact text."""
372
+ isolated = _isolated_release("response", arguments)
373
+ if isolated is not None:
374
+ return isolated
375
+ target = arguments.get("target_text", "")
376
+ language = arguments.get("language", "")
377
+ attestations = arguments.get("attestations") or {}
378
+ if not isinstance(attestations, dict):
379
+ attestations = {}
380
+ target_is_text = isinstance(target, str)
381
+ language_is_exact = (
382
+ isinstance(language, str)
383
+ and language.casefold() not in {"auto", "all"}
384
+ and EXACT_LANGUAGE_TAG.fullmatch(language) is not None
385
+ )
386
+ report = validate_text(
387
+ target if target_is_text else "",
388
+ language if isinstance(language, str) else "",
389
+ arguments.get("glossary"),
390
+ arguments.get("content_type", "prose"),
391
+ arguments.get("short_text_reviewed") is True,
392
+ )
393
+ report["checks"].append("agent-response-native-orthography")
394
+ if not target_is_text:
395
+ report["findings"].append(
396
+ asdict(Finding("invalid-target-type", "target_text must be a string."))
397
+ )
398
+ if not language_is_exact:
399
+ report["findings"].append(
400
+ asdict(Finding(
401
+ "exact-language-required",
402
+ "A host-supplied exact language or locale tag is required for response release.",
403
+ ))
404
+ )
405
+ missing = [name for name in ("nativeness", "orthography") if attestations.get(name) is not True]
406
+ if missing:
407
+ report["findings"].append(
408
+ asdict(Finding(
409
+ "missing-response-attestations",
410
+ "The following response checks were not explicitly passed: " + ", ".join(missing),
411
+ ))
412
+ )
413
+ report["status"] = "BLOCK" if report["findings"] else "PASS"
414
+ report["release_allowed"] = not report["findings"]
415
+ report["required_attestations"] = ["nativeness", "orthography"]
416
+ report["limitations"] = (
417
+ "Deterministic checks cannot prove that every word is native or correctly accented. "
418
+ "Response release also requires nativeness and orthography review plus a trusted host interceptor."
419
+ )
420
+ if report["release_allowed"]:
421
+ key = QUALITY.load_or_create_key(KEY_PATH)
422
+ report["release_token"] = QUALITY.issue_receipt(
423
+ "", target, language, key,
424
+ content_type=arguments.get("content_type", "prose"),
425
+ short_text_reviewed=arguments.get("short_text_reviewed") is True,
426
+ purpose="response",
427
+ )
428
+ return report
429
+
430
+
431
+ TOOLS = [
432
+ {
433
+ "name": "verify_release_token",
434
+ "description": "Cryptographically verify that a BLUN release receipt is authentic, unexpired, and bound to the exact purpose, source when applicable, target, locale, and guard version. Never accept a receipt based on its appearance.",
435
+ "inputSchema": {
436
+ "type": "object",
437
+ "properties": {
438
+ "release_token": {"type": "string"},
439
+ "source_text": {"type": "string"},
440
+ "target_text": {"type": "string"},
441
+ "language": {"type": "string"},
442
+ "purpose": {"type": "string", "enum": ["translation", "response"], "default": "translation"},
443
+ "content_type": {"type": "string", "enum": ["prose", "title", "meta_description", "ui"], "default": "prose"},
444
+ "short_text_reviewed": {"type": "boolean", "default": False},
445
+ },
446
+ "required": ["release_token", "source_text", "target_text", "language"],
447
+ "additionalProperties": False,
448
+ },
449
+ },
450
+ {
451
+ "name": "release_response",
452
+ "description": "Mandatory final gate for an agent's own user-visible natural-language answer. Returns a purpose-bound token only after deterministic Unicode, script, native-diacritics, and explicit nativeness/orthography checks pass. Never use this tool for a translation.",
453
+ "inputSchema": {
454
+ "type": "object",
455
+ "properties": {
456
+ "target_text": {"type": "string"},
457
+ "language": {"type": "string", "description": "Exact BCP 47 language or locale tag supplied by the host; auto and all are rejected."},
458
+ "glossary": {"type": "object"},
459
+ "content_type": {"type": "string", "enum": ["prose", "title", "meta_description", "ui"], "default": "prose"},
460
+ "short_text_reviewed": {"type": "boolean", "description": "Compatibility metadata only; never suppresses measurable findings."},
461
+ "attestations": {
462
+ "type": "object",
463
+ "properties": {
464
+ "nativeness": {"type": "boolean"},
465
+ "orthography": {"type": "boolean"},
466
+ },
467
+ "required": ["nativeness", "orthography"],
468
+ "additionalProperties": False,
469
+ },
470
+ },
471
+ "required": ["target_text", "language", "attestations"],
472
+ "additionalProperties": False,
473
+ },
474
+ },
475
+ {
476
+ "name": "validate_text",
477
+ "description": "Run deterministic Unicode, script-safety, and native-diacritics checks on target-language text.",
478
+ "inputSchema": {
479
+ "type": "object",
480
+ "properties": {
481
+ "text": {"type": "string"},
482
+ "language": {"type": "string", "default": "auto"},
483
+ "glossary": {"type": "object", "description": "Optional source-term to required target-term or regex-rule map."},
484
+ "content_type": {"type": "string", "enum": ["prose", "title", "meta_description", "ui"], "default": "prose"},
485
+ "short_text_reviewed": {"type": "boolean", "default": False},
486
+ },
487
+ "required": ["text"],
488
+ "additionalProperties": False,
489
+ },
490
+ },
491
+ {
492
+ "name": "release_translation",
493
+ "description": "Mandatory final gate. Returns a release token only after deterministic validation, whole-input and structured-segment source-target non-identity, auto-detected translation-volume integrity, and all seven quality attestations pass.",
494
+ "inputSchema": {
495
+ "type": "object",
496
+ "properties": {
497
+ "source_text": {"type": "string"},
498
+ "target_text": {"type": "string"},
499
+ "language": {"type": "string"},
500
+ "content_type": {"type": "string", "enum": ["prose", "title", "meta_description", "ui"], "default": "prose"},
501
+ "short_text_reviewed": {"type": "boolean", "description": "Compatibility metadata only. Never suppresses measurable findings and is not independent proof."},
502
+ "attestations": {
503
+ "type": "object",
504
+ "properties": {
505
+ name: {"type": "boolean"}
506
+ for name in (
507
+ "meaning",
508
+ "completeness",
509
+ "precision",
510
+ "nativeness",
511
+ "locale_fit",
512
+ "integrity",
513
+ "orthography",
514
+ )
515
+ },
516
+ "required": [
517
+ "meaning",
518
+ "completeness",
519
+ "precision",
520
+ "nativeness",
521
+ "locale_fit",
522
+ "integrity",
523
+ "orthography",
524
+ ],
525
+ "additionalProperties": False,
526
+ },
527
+ },
528
+ "required": ["source_text", "target_text", "language", "attestations"],
529
+ "additionalProperties": False,
530
+ },
531
+ },
532
+ ]
533
+
534
+
535
+ def _tool_result(payload: dict[str, Any]) -> dict[str, Any]:
536
+ return {
537
+ "content": [{"type": "text", "text": json.dumps(payload, ensure_ascii=False)}],
538
+ "structuredContent": payload,
539
+ "isError": payload.get("status") != "PASS",
540
+ }
541
+
542
+
543
+ def handle_message(message: dict[str, Any]) -> dict[str, Any] | None:
544
+ method = message.get("method")
545
+ request_id = message.get("id")
546
+ if request_id is None:
547
+ return None
548
+ if method == "initialize":
549
+ params = message.get("params") if isinstance(message.get("params"), dict) else {}
550
+ requested_protocol = params.get("protocolVersion")
551
+ negotiated_protocol = (
552
+ requested_protocol
553
+ if isinstance(requested_protocol, str) and requested_protocol in SUPPORTED_PROTOCOL_VERSIONS
554
+ else PROTOCOL_VERSION
555
+ )
556
+ return {
557
+ "jsonrpc": "2.0",
558
+ "id": request_id,
559
+ "result": {
560
+ "protocolVersion": negotiated_protocol,
561
+ "capabilities": {
562
+ "tools": {"listChanged": False},
563
+ "prompts": {"listChanged": False},
564
+ },
565
+ "serverInfo": {"name": "blun-language-guard", "version": VERSION},
566
+ "instructions": MCP_INSTRUCTIONS,
567
+ },
568
+ }
569
+ if method == "ping":
570
+ return {"jsonrpc": "2.0", "id": request_id, "result": {}}
571
+ if method == "tools/list":
572
+ return {"jsonrpc": "2.0", "id": request_id, "result": {"tools": TOOLS}}
573
+ if method == "prompts/list":
574
+ return {
575
+ "jsonrpc": "2.0",
576
+ "id": request_id,
577
+ "result": {"prompts": [{
578
+ "name": "translate-native",
579
+ "title": "Translate Native mandatory workflow",
580
+ "description": "Load the native translation workflow before drafting any translation.",
581
+ "arguments": [],
582
+ }]},
583
+ }
584
+ if method == "prompts/get":
585
+ params = message.get("params") or {}
586
+ if params.get("name") != "translate-native":
587
+ return {
588
+ "jsonrpc": "2.0",
589
+ "id": request_id,
590
+ "error": {"code": -32602, "message": "Unknown prompt"},
591
+ }
592
+ return {
593
+ "jsonrpc": "2.0",
594
+ "id": request_id,
595
+ "result": {
596
+ "description": "Mandatory native translation and orthography workflow.",
597
+ "messages": [{
598
+ "role": "user",
599
+ "content": {"type": "text", "text": MCP_INSTRUCTIONS},
600
+ }],
601
+ },
602
+ }
603
+ if method == "tools/call":
604
+ params = message.get("params") or {}
605
+ name = params.get("name")
606
+ arguments = params.get("arguments") or {}
607
+ if name == "validate_text":
608
+ payload = validate_text(
609
+ arguments.get("text", ""), arguments.get("language", "auto"), arguments.get("glossary"),
610
+ arguments.get("content_type", "prose"), arguments.get("short_text_reviewed") is True,
611
+ )
612
+ elif name == "release_translation":
613
+ payload = release_translation(arguments)
614
+ elif name == "release_response":
615
+ payload = release_response(arguments)
616
+ elif name == "verify_release_token":
617
+ if SERVICE_ENDPOINT:
618
+ try:
619
+ payload = SERVICE_CLIENT.call_guard_service(
620
+ SERVICE_ENDPOINT,
621
+ {
622
+ "operation": "verify",
623
+ "task_kind": arguments.get("purpose", "translation"),
624
+ "source_text": arguments.get("source_text", ""),
625
+ "target_text": arguments.get("target_text", ""),
626
+ "language": arguments.get("language", ""),
627
+ "release_token": arguments.get("release_token", ""),
628
+ "content_type": arguments.get("content_type", "prose"),
629
+ "short_text_reviewed": arguments.get("short_text_reviewed") is True,
630
+ "agent_id": os.environ.get("BLUN_LANGUAGE_GUARD_AGENT_ID", ""),
631
+ "channel": os.environ.get("BLUN_LANGUAGE_GUARD_CHANNEL", "mcp"),
632
+ },
633
+ auth_token=_service_token(),
634
+ )
635
+ except (OSError, SERVICE_CLIENT.GuardServiceError) as error:
636
+ payload = {"valid": False, "status": "BLOCK", "error": str(error)}
637
+ else:
638
+ payload = QUALITY.verify_receipt(
639
+ arguments.get("release_token", ""),
640
+ arguments.get("source_text", ""),
641
+ arguments.get("target_text", ""),
642
+ arguments.get("language", ""),
643
+ QUALITY.load_or_create_key(KEY_PATH),
644
+ arguments.get("content_type", "prose"),
645
+ arguments.get("short_text_reviewed") is True,
646
+ arguments.get("purpose", "translation"),
647
+ )
648
+ payload["status"] = "PASS" if payload.get("valid") else "BLOCK"
649
+ else:
650
+ return {
651
+ "jsonrpc": "2.0",
652
+ "id": request_id,
653
+ "error": {"code": -32601, "message": f"Unknown tool: {name}"},
654
+ }
655
+ return {"jsonrpc": "2.0", "id": request_id, "result": _tool_result(payload)}
656
+ return {
657
+ "jsonrpc": "2.0",
658
+ "id": request_id,
659
+ "error": {"code": -32601, "message": f"Unknown method: {method}"},
660
+ }
661
+
662
+
663
+ def serve() -> int:
664
+ for line in sys.stdin:
665
+ if not line.strip():
666
+ continue
667
+ try:
668
+ response = handle_message(json.loads(line.lstrip("\ufeff")))
669
+ except Exception as error: # Keep the MCP process alive after malformed input.
670
+ response = {
671
+ "jsonrpc": "2.0",
672
+ "id": None,
673
+ "error": {"code": -32603, "message": str(error)},
674
+ }
675
+ if response is not None:
676
+ print(json.dumps(response, ensure_ascii=False), flush=True)
677
+ return 0
678
+
679
+
680
+ def main() -> int:
681
+ parser = argparse.ArgumentParser(description="BLUN Language Guard")
682
+ subparsers = parser.add_subparsers(dest="command", required=True)
683
+ subparsers.add_parser("serve", help="Run the MCP server over stdio")
684
+ validate = subparsers.add_parser("validate", help="Validate text from a file or stdin")
685
+ validate.add_argument("path", nargs="?", type=Path)
686
+ validate.add_argument("--language", default="auto")
687
+ args = parser.parse_args()
688
+ if args.command == "serve":
689
+ return serve()
690
+ text = args.path.read_text(encoding="utf-8") if args.path else sys.stdin.read()
691
+ report = validate_text(text, args.language)
692
+ print(json.dumps(report, ensure_ascii=False, indent=2))
693
+ return 0 if report["release_allowed"] else 1
694
+
695
+
696
+ if __name__ == "__main__":
697
+ raise SystemExit(main())