blun-king-cli 9.1.563 → 9.1.565

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 (85) hide show
  1. package/agent-spine-plugin/.claude-plugin/marketplace.json +20 -20
  2. package/agent-spine-plugin/.claude-plugin/plugin.json +14 -14
  3. package/agent-spine-plugin/.codex-plugin/plugin.json +34 -34
  4. package/agent-spine-plugin/.mcp.json +8 -8
  5. package/agent-spine-plugin/CONTRIBUTING.md +52 -0
  6. package/agent-spine-plugin/LICENSE +186 -186
  7. package/agent-spine-plugin/README.md +394 -0
  8. package/agent-spine-plugin/SECURITY.md +47 -0
  9. package/agent-spine-plugin/assets/agentspine-banner.svg +32 -32
  10. package/agent-spine-plugin/bin/agentspine-mcp.js +4 -4
  11. package/agent-spine-plugin/bin/agentspine.js +7 -7
  12. package/agent-spine-plugin/blun.plugin.json +33 -33
  13. package/agent-spine-plugin/hooks/codex.json +47 -47
  14. package/agent-spine-plugin/hooks/hooks.json +106 -106
  15. package/agent-spine-plugin/hooks/version.json +5 -5
  16. package/agent-spine-plugin/package.json +69 -69
  17. package/agent-spine-plugin/scripts/check-hosts.js +199 -199
  18. package/agent-spine-plugin/skill/SKILL.md +76 -76
  19. package/agent-spine-plugin/skills/agent-spine/SKILL.md +85 -85
  20. package/agent-spine-plugin/src/cli.js +1488 -1442
  21. package/agent-spine-plugin/src/hook.js +886 -812
  22. package/agent-spine-plugin/src/index.js +93 -93
  23. package/agent-spine-plugin/src/lib/acceptance.js +333 -333
  24. package/agent-spine-plugin/src/lib/attention.js +755 -755
  25. package/agent-spine-plugin/src/lib/audit.js +351 -342
  26. package/agent-spine-plugin/src/lib/authentication.js +515 -515
  27. package/agent-spine-plugin/src/lib/briefing.js +317 -317
  28. package/agent-spine-plugin/src/lib/catalog.js +167 -167
  29. package/agent-spine-plugin/src/lib/channel-runtime.js +665 -665
  30. package/agent-spine-plugin/src/lib/context.js +154 -154
  31. package/agent-spine-plugin/src/lib/continuity.js +338 -338
  32. package/agent-spine-plugin/src/lib/coordination.js +577 -577
  33. package/agent-spine-plugin/src/lib/documents.js +217 -217
  34. package/agent-spine-plugin/src/lib/feed-transport.js +499 -499
  35. package/agent-spine-plugin/src/lib/filesystem-retry.js +32 -32
  36. package/agent-spine-plugin/src/lib/gateway-runtime.js +1119 -760
  37. package/agent-spine-plugin/src/lib/graph.js +337 -337
  38. package/agent-spine-plugin/src/lib/hook-audit.js +29 -0
  39. package/agent-spine-plugin/src/lib/https-transport.js +392 -392
  40. package/agent-spine-plugin/src/lib/indexed-memory-offline.js +40 -40
  41. package/agent-spine-plugin/src/lib/indexed-memory.js +281 -281
  42. package/agent-spine-plugin/src/lib/learning.js +6923 -6466
  43. package/agent-spine-plugin/src/lib/object-transport.js +206 -206
  44. package/agent-spine-plugin/src/lib/owned-file-lock.js +143 -143
  45. package/agent-spine-plugin/src/lib/paths.js +109 -109
  46. package/agent-spine-plugin/src/lib/peer-transport.js +283 -283
  47. package/agent-spine-plugin/src/lib/persona-runtime.js +581 -581
  48. package/agent-spine-plugin/src/lib/preflight.js +702 -702
  49. package/agent-spine-plugin/src/lib/runtime.js +13 -13
  50. package/agent-spine-plugin/src/lib/selfstarter.js +892 -819
  51. package/agent-spine-plugin/src/lib/sharing.js +969 -969
  52. package/agent-spine-plugin/src/lib/source-roots.js +529 -482
  53. package/agent-spine-plugin/src/lib/sqlite-transport.js +501 -501
  54. package/agent-spine-plugin/src/lib/telegram-adapter.js +119 -119
  55. package/agent-spine-plugin/src/lib/voice-runtime.js +39 -39
  56. package/agent-spine-plugin/src/mcp.js +597 -572
  57. package/agent-spine-plugin/src/version.js +1 -1
  58. package/agent-spine-plugin/src/worker.js +202 -195
  59. package/bin/active-steer-priority-policy.cjs +24 -0
  60. package/bin/launcher-runtime.js +8 -1
  61. package/bin/mnemo-tool-agent-policy.cjs +22 -0
  62. package/bin/thinking-activity-status-policy.cjs +132 -0
  63. package/bin/thinking-only-guard.cjs +75 -0
  64. package/bin/tool-call-loop-policy.cjs +53 -0
  65. package/bin/turn-thinking-policy.cjs +25 -1
  66. package/blun.mjs +554 -64
  67. package/package.json +4 -1
  68. package/standard-skills/translate-native/LICENSE +21 -21
  69. package/standard-skills/translate-native/references/evaluation-protocol.md +95 -95
  70. package/standard-skills/translate-native/references/native-orthography.md +79 -79
  71. package/standard-skills/translate-native/references/native-translation-standard.md +94 -94
  72. package/standard-skills/translate-native/references/structured-content.md +72 -72
  73. package/standard-skills/translate-native/references/translationese-review.md +77 -77
  74. package/standard-skills/translate-native/scripts/blun_language_guard.py +697 -697
  75. package/standard-skills/translate-native/scripts/check_diacritics.py +353 -353
  76. package/standard-skills/translate-native/scripts/guard_service_client.py +264 -264
  77. package/standard-skills/translate-native/scripts/language_gateway.py +62 -62
  78. package/standard-skills/translate-native/scripts/language_quality.py +377 -377
  79. package/standard-skills/translate-native/scripts/pre_output_guard.py +64 -64
  80. package/standard-skills/translate-native/scripts/translation_guard.py +916 -916
  81. package/standard-tools/language-guard/blun_language_guard.py +697 -697
  82. package/standard-tools/language-guard/check_diacritics.py +353 -353
  83. package/standard-tools/language-guard/guard_service_client.py +264 -264
  84. package/standard-tools/language-guard/language_quality.py +377 -377
  85. 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())