blun-king-cli 9.1.6 → 9.1.9

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 (28) hide show
  1. package/bin/launcher-mode.js +1 -0
  2. package/bin/launcher-runtime.js +69 -29
  3. package/bin/standard-tools-bootstrap.js +218 -0
  4. package/blun.mjs +4 -2
  5. package/package.json +50 -47
  6. package/standard-skills/blun-app-design-system/SKILL.md +30 -0
  7. package/standard-skills/blun-app-design-system/provenance.json +17 -0
  8. package/standard-skills/blun-app-design-system/references/tokens.md +48 -0
  9. package/standard-skills/blun-app-design-system/scripts/check-blun-design.mjs +49 -0
  10. package/standard-tools/language-guard/blun_language_guard.py +686 -0
  11. package/standard-tools/language-guard/check_diacritics.py +353 -0
  12. package/standard-tools/language-guard/guard_service_client.py +82 -0
  13. package/standard-tools/language-guard/language_quality.py +172 -0
  14. package/standard-tools/language-guard/translation_guard.py +916 -0
  15. package/standard-tools/manifest.json +76 -0
  16. package/skills/design-taste-frontend/SKILL.md +0 -1206
  17. package/skills/full-output-enforcement/SKILL.md +0 -49
  18. package/skills/high-end-visual-design/SKILL.md +0 -98
  19. package/skills/image-to-code/SKILL.md +0 -1228
  20. package/skills/industrial-brutalist-ui/SKILL.md +0 -92
  21. package/skills/minimalist-ui/SKILL.md +0 -85
  22. package/skills/motion-design-taste/SKILL.md +0 -74
  23. package/skills/premortem/SKILL.md +0 -148
  24. package/skills/redesign-existing-projects/SKILL.md +0 -178
  25. package/skills/screenshot-lesen/SKILL.md +0 -52
  26. package/skills/stitch-design-taste/DESIGN.md +0 -121
  27. package/skills/stitch-design-taste/SKILL.md +0 -184
  28. package/skills/web-lesen/SKILL.md +0 -58
@@ -0,0 +1,686 @@
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
+ "de": {"patterns": (r"ae", r"oe", r"ue"), "native": "äöüÄÖÜ", "minimum": 3},
99
+ "sv": {"patterns": (r"aa", r"ae", r"oe"), "native": "åäöÅÄÖ", "minimum": 1},
100
+ "da": {"patterns": (r"aa", r"ae", r"oe"), "native": "åæøÅÆØ", "minimum": 1},
101
+ "no": {"patterns": (r"aa", r"ae", r"oe"), "native": "åæøÅÆØ", "minimum": 1},
102
+ }
103
+
104
+
105
+ @dataclass(frozen=True)
106
+ class Finding:
107
+ code: str
108
+ message: str
109
+ blocking: bool = True
110
+ line: int | None = None
111
+ language: str | None = None
112
+
113
+
114
+ def _service_token() -> str:
115
+ direct = os.environ.get("BLUN_LANGUAGE_GUARD_SERVICE_TOKEN", "").strip()
116
+ if direct:
117
+ return direct
118
+ if not SERVICE_TOKEN_FILE:
119
+ return ""
120
+ token = Path(SERVICE_TOKEN_FILE).read_text(encoding="utf-8-sig").strip()
121
+ if len(token) < 32:
122
+ raise SERVICE_CLIENT.GuardServiceError("guard service token is invalid")
123
+ return token
124
+
125
+
126
+ def _isolated_release(task_kind: str, arguments: dict[str, Any]) -> dict[str, Any] | None:
127
+ if not SERVICE_ENDPOINT:
128
+ return None
129
+ request = dict(arguments)
130
+ request.update({
131
+ "operation": "release",
132
+ "task_kind": task_kind,
133
+ "agent_id": os.environ.get("BLUN_LANGUAGE_GUARD_AGENT_ID", ""),
134
+ "channel": os.environ.get("BLUN_LANGUAGE_GUARD_CHANNEL", "mcp"),
135
+ })
136
+ if task_kind == "response":
137
+ request["source_text"] = ""
138
+ try:
139
+ return SERVICE_CLIENT.call_guard_service(
140
+ SERVICE_ENDPOINT,
141
+ request,
142
+ auth_token=_service_token(),
143
+ )
144
+ except (OSError, SERVICE_CLIENT.GuardServiceError) as error:
145
+ return {
146
+ "status": "BLOCK",
147
+ "release_allowed": False,
148
+ "reason": "isolated-guard-unavailable",
149
+ "error": str(error),
150
+ }
151
+
152
+
153
+ def _languages_for(text: str, language: str) -> tuple[str, ...]:
154
+ if language == "all":
155
+ return tuple(DIACRITICS.RULES)
156
+ if language == "auto":
157
+ return DIACRITICS.detect_languages(text)
158
+ base = language.casefold().split("-", 1)[0].split("_", 1)[0]
159
+ return (base,) if base in DIACRITICS.RULES else ()
160
+
161
+
162
+ def validate_text(
163
+ text: str,
164
+ language: str = "auto",
165
+ glossary: dict[str, Any] | None = None,
166
+ content_type: str = "prose",
167
+ short_text_reviewed: bool = False,
168
+ ) -> dict[str, Any]:
169
+ findings: list[Finding] = []
170
+ if not text.strip():
171
+ findings.append(Finding("empty-target", "Target text is empty."))
172
+ if text != unicodedata.normalize("NFC", text):
173
+ findings.append(Finding("unicode-not-nfc", "Target text is not NFC-normalized."))
174
+ if "\ufffd" in text:
175
+ findings.append(Finding("replacement-character", "Target contains U+FFFD replacement characters."))
176
+ if "\x00" in text:
177
+ findings.append(Finding("nul-character", "Target contains a NUL character."))
178
+
179
+ for bidi in QUALITY.bidi_findings(text):
180
+ findings.append(Finding(bidi["code"], json.dumps(bidi, ensure_ascii=False)))
181
+
182
+ script = QUALITY.script_report(text, language)
183
+ if script.get("status") == "fail":
184
+ findings.append(Finding("script-mismatch", json.dumps(script, ensure_ascii=False), language=language))
185
+ base_language = language.casefold().replace("_", "-").split("-", 1)[0]
186
+ profile = LANGUAGE_CHARACTER_PROFILES.get(base_language)
187
+ profile_prose = DIACRITICS.mask_technical_text(text)
188
+ if profile and len(profile_prose) >= 200 and not any(character in profile for character in profile_prose):
189
+ findings.append(Finding(
190
+ "missing-language-character-profile",
191
+ f"Long {base_language} text contains none of the language's characteristic native characters; possible wholesale ASCII folding.",
192
+ language=language,
193
+ ))
194
+ folding_profile = ASCII_FOLDING_PROFILES.get(base_language)
195
+ if folding_profile:
196
+ folded = sum(len(re.findall(pattern, profile_prose, re.IGNORECASE)) for pattern in folding_profile["patterns"])
197
+ native = sum(profile_prose.count(character) for character in folding_profile["native"])
198
+ if folded >= folding_profile["minimum"] and folded > native:
199
+ findings.append(Finding(
200
+ "ascii-folding-pressure",
201
+ f"Measured ASCII-folding candidates ({folded}) exceed native characters ({native}); review the exact spelling.",
202
+ language=language,
203
+ ))
204
+ # Kept as compatibility metadata only. It never suppresses a measurable finding.
205
+ short_sensitive = content_type in {"title", "meta_description", "ui"} and len(profile_prose.strip()) < 200
206
+ if short_sensitive and not short_text_reviewed and not findings:
207
+ findings.append(Finding(
208
+ "short-text-native-review-required",
209
+ f"Short {content_type} text needs host-enforced review; an MCP Boolean is not independent proof.",
210
+ language=language,
211
+ ))
212
+ for glossary_finding in QUALITY.glossary_findings(
213
+ text, glossary if isinstance(glossary, dict) else {}
214
+ ):
215
+ findings.append(Finding(glossary_finding["code"], json.dumps(glossary_finding, ensure_ascii=False), language=language))
216
+
217
+ prose = DIACRITICS.mask_technical_text(text)
218
+ for line, code, found, suggestion in DIACRITICS.iter_findings(
219
+ prose, _languages_for(prose, language)
220
+ ):
221
+ findings.append(
222
+ Finding(
223
+ "suspected-ascii-substitution",
224
+ f"{found!r} may require native spelling {suggestion!r}.",
225
+ line=line,
226
+ language=code,
227
+ )
228
+ )
229
+
230
+ return {
231
+ "status": (
232
+ "REVIEW_REQUIRED"
233
+ if findings and all(finding.code == "short-text-native-review-required" for finding in findings)
234
+ else "BLOCK" if findings else "PASS"
235
+ ),
236
+ "release_allowed": not findings,
237
+ "language": language,
238
+ "checks": [
239
+ "non-empty",
240
+ "unicode-nfc",
241
+ "encoding-integrity",
242
+ "bidi-control-safety",
243
+ "native-diacritics-heuristics",
244
+ ],
245
+ "findings": [asdict(finding) for finding in findings],
246
+ "limitations": (
247
+ "Deterministic checks cannot prove semantic fidelity or native fluency. "
248
+ "The release gate therefore also requires explicit seven-pass attestations."
249
+ ),
250
+ }
251
+
252
+
253
+ def release_translation(arguments: dict[str, Any]) -> dict[str, Any]:
254
+ isolated = _isolated_release("translation", arguments)
255
+ if isolated is not None:
256
+ return isolated
257
+ source = arguments.get("source_text", "")
258
+ target = arguments.get("target_text", "")
259
+ language = arguments.get("language", "auto")
260
+ source_is_text = isinstance(source, str)
261
+ target_is_text = isinstance(target, str)
262
+ language_is_exact = (
263
+ isinstance(language, str)
264
+ and language.casefold() not in {"auto", "all"}
265
+ and EXACT_LANGUAGE_TAG.fullmatch(language) is not None
266
+ )
267
+ source = source if source_is_text else ""
268
+ target = target if target_is_text else ""
269
+ language = language if isinstance(language, str) else ""
270
+ attestations = arguments.get("attestations") or {}
271
+ if not isinstance(attestations, dict):
272
+ attestations = {}
273
+ required = (
274
+ "meaning",
275
+ "completeness",
276
+ "precision",
277
+ "nativeness",
278
+ "locale_fit",
279
+ "integrity",
280
+ "orthography",
281
+ )
282
+ report = validate_text(
283
+ target,
284
+ language,
285
+ arguments.get("glossary"),
286
+ arguments.get("content_type", "prose"),
287
+ arguments.get("short_text_reviewed") is True,
288
+ )
289
+ report["checks"].extend([
290
+ "source-target-identity",
291
+ "structured-segment-identity",
292
+ "translation-volume-integrity",
293
+ ])
294
+ if not source_is_text:
295
+ report["findings"].append(
296
+ asdict(Finding("invalid-source-type", "source_text must be a string."))
297
+ )
298
+ if not target_is_text:
299
+ report["findings"].append(
300
+ asdict(Finding("invalid-target-type", "target_text must be a string."))
301
+ )
302
+ if not language_is_exact:
303
+ report["findings"].append(
304
+ asdict(Finding(
305
+ "exact-language-required",
306
+ "A host-supplied exact language or locale tag is required for translation release.",
307
+ ))
308
+ )
309
+ missing = [name for name in required if attestations.get(name) is not True]
310
+ if not source.strip():
311
+ report["findings"].append(
312
+ asdict(Finding("empty-source", "Source text is required for the fidelity gate."))
313
+ )
314
+ else:
315
+ whole_identity_errors = TRANSLATION.identity_errors(source, target)
316
+ for error in whole_identity_errors:
317
+ report["findings"].append(
318
+ asdict(Finding("source-target-identical", error))
319
+ )
320
+ if not whole_identity_errors:
321
+ selected_format = TRANSLATION.detect_content_format(source)
322
+ for error in TRANSLATION.structured_identity_errors(
323
+ source, target, selected_format
324
+ ):
325
+ report["findings"].append(
326
+ asdict(Finding("unchanged-linguistic-segment", error))
327
+ )
328
+ for error in TRANSLATION.translation_volume_errors(source, target):
329
+ report["findings"].append(
330
+ asdict(Finding("translation-volume-integrity", error))
331
+ )
332
+ if missing:
333
+ report["findings"].append(
334
+ asdict(
335
+ Finding(
336
+ "missing-attestations",
337
+ "The following release checks were not explicitly passed: "
338
+ + ", ".join(missing),
339
+ )
340
+ )
341
+ )
342
+ review_only = report["findings"] and all(
343
+ finding.get("code") == "short-text-native-review-required" for finding in report["findings"]
344
+ )
345
+ report["status"] = "REVIEW_REQUIRED" if review_only else "BLOCK" if report["findings"] else "PASS"
346
+ report["release_allowed"] = not report["findings"]
347
+ report["required_attestations"] = list(required)
348
+ if report["release_allowed"]:
349
+ key = QUALITY.load_or_create_key(KEY_PATH)
350
+ report["release_token"] = QUALITY.issue_receipt(
351
+ source, target, language, key,
352
+ content_type=arguments.get("content_type", "prose"),
353
+ short_text_reviewed=arguments.get("short_text_reviewed") is True,
354
+ purpose="translation",
355
+ )
356
+ return report
357
+
358
+
359
+ def release_response(arguments: dict[str, Any]) -> dict[str, Any]:
360
+ """Validate an agent's own final answer and bind a receipt to the exact text."""
361
+ isolated = _isolated_release("response", arguments)
362
+ if isolated is not None:
363
+ return isolated
364
+ target = arguments.get("target_text", "")
365
+ language = arguments.get("language", "")
366
+ attestations = arguments.get("attestations") or {}
367
+ if not isinstance(attestations, dict):
368
+ attestations = {}
369
+ target_is_text = isinstance(target, str)
370
+ language_is_exact = (
371
+ isinstance(language, str)
372
+ and language.casefold() not in {"auto", "all"}
373
+ and EXACT_LANGUAGE_TAG.fullmatch(language) is not None
374
+ )
375
+ report = validate_text(
376
+ target if target_is_text else "",
377
+ language if isinstance(language, str) else "",
378
+ arguments.get("glossary"),
379
+ arguments.get("content_type", "prose"),
380
+ arguments.get("short_text_reviewed") is True,
381
+ )
382
+ report["checks"].append("agent-response-native-orthography")
383
+ if not target_is_text:
384
+ report["findings"].append(
385
+ asdict(Finding("invalid-target-type", "target_text must be a string."))
386
+ )
387
+ if not language_is_exact:
388
+ report["findings"].append(
389
+ asdict(Finding(
390
+ "exact-language-required",
391
+ "A host-supplied exact language or locale tag is required for response release.",
392
+ ))
393
+ )
394
+ missing = [name for name in ("nativeness", "orthography") if attestations.get(name) is not True]
395
+ if missing:
396
+ report["findings"].append(
397
+ asdict(Finding(
398
+ "missing-response-attestations",
399
+ "The following response checks were not explicitly passed: " + ", ".join(missing),
400
+ ))
401
+ )
402
+ report["status"] = "BLOCK" if report["findings"] else "PASS"
403
+ report["release_allowed"] = not report["findings"]
404
+ report["required_attestations"] = ["nativeness", "orthography"]
405
+ report["limitations"] = (
406
+ "Deterministic checks cannot prove that every word is native or correctly accented. "
407
+ "Response release also requires nativeness and orthography review plus a trusted host interceptor."
408
+ )
409
+ if report["release_allowed"]:
410
+ key = QUALITY.load_or_create_key(KEY_PATH)
411
+ report["release_token"] = QUALITY.issue_receipt(
412
+ "", target, language, key,
413
+ content_type=arguments.get("content_type", "prose"),
414
+ short_text_reviewed=arguments.get("short_text_reviewed") is True,
415
+ purpose="response",
416
+ )
417
+ return report
418
+
419
+
420
+ TOOLS = [
421
+ {
422
+ "name": "verify_release_token",
423
+ "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.",
424
+ "inputSchema": {
425
+ "type": "object",
426
+ "properties": {
427
+ "release_token": {"type": "string"},
428
+ "source_text": {"type": "string"},
429
+ "target_text": {"type": "string"},
430
+ "language": {"type": "string"},
431
+ "purpose": {"type": "string", "enum": ["translation", "response"], "default": "translation"},
432
+ "content_type": {"type": "string", "enum": ["prose", "title", "meta_description", "ui"], "default": "prose"},
433
+ "short_text_reviewed": {"type": "boolean", "default": False},
434
+ },
435
+ "required": ["release_token", "source_text", "target_text", "language"],
436
+ "additionalProperties": False,
437
+ },
438
+ },
439
+ {
440
+ "name": "release_response",
441
+ "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.",
442
+ "inputSchema": {
443
+ "type": "object",
444
+ "properties": {
445
+ "target_text": {"type": "string"},
446
+ "language": {"type": "string", "description": "Exact BCP 47 language or locale tag supplied by the host; auto and all are rejected."},
447
+ "glossary": {"type": "object"},
448
+ "content_type": {"type": "string", "enum": ["prose", "title", "meta_description", "ui"], "default": "prose"},
449
+ "short_text_reviewed": {"type": "boolean", "description": "Compatibility metadata only; never suppresses measurable findings."},
450
+ "attestations": {
451
+ "type": "object",
452
+ "properties": {
453
+ "nativeness": {"type": "boolean"},
454
+ "orthography": {"type": "boolean"},
455
+ },
456
+ "required": ["nativeness", "orthography"],
457
+ "additionalProperties": False,
458
+ },
459
+ },
460
+ "required": ["target_text", "language", "attestations"],
461
+ "additionalProperties": False,
462
+ },
463
+ },
464
+ {
465
+ "name": "validate_text",
466
+ "description": "Run deterministic Unicode, script-safety, and native-diacritics checks on target-language text.",
467
+ "inputSchema": {
468
+ "type": "object",
469
+ "properties": {
470
+ "text": {"type": "string"},
471
+ "language": {"type": "string", "default": "auto"},
472
+ "glossary": {"type": "object", "description": "Optional source-term to required target-term or regex-rule map."},
473
+ "content_type": {"type": "string", "enum": ["prose", "title", "meta_description", "ui"], "default": "prose"},
474
+ "short_text_reviewed": {"type": "boolean", "default": False},
475
+ },
476
+ "required": ["text"],
477
+ "additionalProperties": False,
478
+ },
479
+ },
480
+ {
481
+ "name": "release_translation",
482
+ "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.",
483
+ "inputSchema": {
484
+ "type": "object",
485
+ "properties": {
486
+ "source_text": {"type": "string"},
487
+ "target_text": {"type": "string"},
488
+ "language": {"type": "string"},
489
+ "content_type": {"type": "string", "enum": ["prose", "title", "meta_description", "ui"], "default": "prose"},
490
+ "short_text_reviewed": {"type": "boolean", "description": "Compatibility metadata only. Never suppresses measurable findings and is not independent proof."},
491
+ "attestations": {
492
+ "type": "object",
493
+ "properties": {
494
+ name: {"type": "boolean"}
495
+ for name in (
496
+ "meaning",
497
+ "completeness",
498
+ "precision",
499
+ "nativeness",
500
+ "locale_fit",
501
+ "integrity",
502
+ "orthography",
503
+ )
504
+ },
505
+ "required": [
506
+ "meaning",
507
+ "completeness",
508
+ "precision",
509
+ "nativeness",
510
+ "locale_fit",
511
+ "integrity",
512
+ "orthography",
513
+ ],
514
+ "additionalProperties": False,
515
+ },
516
+ },
517
+ "required": ["source_text", "target_text", "language", "attestations"],
518
+ "additionalProperties": False,
519
+ },
520
+ },
521
+ ]
522
+
523
+
524
+ def _tool_result(payload: dict[str, Any]) -> dict[str, Any]:
525
+ return {
526
+ "content": [{"type": "text", "text": json.dumps(payload, ensure_ascii=False)}],
527
+ "structuredContent": payload,
528
+ "isError": payload.get("status") != "PASS",
529
+ }
530
+
531
+
532
+ def handle_message(message: dict[str, Any]) -> dict[str, Any] | None:
533
+ method = message.get("method")
534
+ request_id = message.get("id")
535
+ if request_id is None:
536
+ return None
537
+ if method == "initialize":
538
+ params = message.get("params") if isinstance(message.get("params"), dict) else {}
539
+ requested_protocol = params.get("protocolVersion")
540
+ negotiated_protocol = (
541
+ requested_protocol
542
+ if isinstance(requested_protocol, str) and requested_protocol in SUPPORTED_PROTOCOL_VERSIONS
543
+ else PROTOCOL_VERSION
544
+ )
545
+ return {
546
+ "jsonrpc": "2.0",
547
+ "id": request_id,
548
+ "result": {
549
+ "protocolVersion": negotiated_protocol,
550
+ "capabilities": {
551
+ "tools": {"listChanged": False},
552
+ "prompts": {"listChanged": False},
553
+ },
554
+ "serverInfo": {"name": "blun-language-guard", "version": VERSION},
555
+ "instructions": MCP_INSTRUCTIONS,
556
+ },
557
+ }
558
+ if method == "ping":
559
+ return {"jsonrpc": "2.0", "id": request_id, "result": {}}
560
+ if method == "tools/list":
561
+ return {"jsonrpc": "2.0", "id": request_id, "result": {"tools": TOOLS}}
562
+ if method == "prompts/list":
563
+ return {
564
+ "jsonrpc": "2.0",
565
+ "id": request_id,
566
+ "result": {"prompts": [{
567
+ "name": "translate-native",
568
+ "title": "Translate Native mandatory workflow",
569
+ "description": "Load the native translation workflow before drafting any translation.",
570
+ "arguments": [],
571
+ }]},
572
+ }
573
+ if method == "prompts/get":
574
+ params = message.get("params") or {}
575
+ if params.get("name") != "translate-native":
576
+ return {
577
+ "jsonrpc": "2.0",
578
+ "id": request_id,
579
+ "error": {"code": -32602, "message": "Unknown prompt"},
580
+ }
581
+ return {
582
+ "jsonrpc": "2.0",
583
+ "id": request_id,
584
+ "result": {
585
+ "description": "Mandatory native translation and orthography workflow.",
586
+ "messages": [{
587
+ "role": "user",
588
+ "content": {"type": "text", "text": MCP_INSTRUCTIONS},
589
+ }],
590
+ },
591
+ }
592
+ if method == "tools/call":
593
+ params = message.get("params") or {}
594
+ name = params.get("name")
595
+ arguments = params.get("arguments") or {}
596
+ if name == "validate_text":
597
+ payload = validate_text(
598
+ arguments.get("text", ""), arguments.get("language", "auto"), arguments.get("glossary"),
599
+ arguments.get("content_type", "prose"), arguments.get("short_text_reviewed") is True,
600
+ )
601
+ elif name == "release_translation":
602
+ payload = release_translation(arguments)
603
+ elif name == "release_response":
604
+ payload = release_response(arguments)
605
+ elif name == "verify_release_token":
606
+ if SERVICE_ENDPOINT:
607
+ try:
608
+ payload = SERVICE_CLIENT.call_guard_service(
609
+ SERVICE_ENDPOINT,
610
+ {
611
+ "operation": "verify",
612
+ "task_kind": arguments.get("purpose", "translation"),
613
+ "source_text": arguments.get("source_text", ""),
614
+ "target_text": arguments.get("target_text", ""),
615
+ "language": arguments.get("language", ""),
616
+ "release_token": arguments.get("release_token", ""),
617
+ "content_type": arguments.get("content_type", "prose"),
618
+ "short_text_reviewed": arguments.get("short_text_reviewed") is True,
619
+ "agent_id": os.environ.get("BLUN_LANGUAGE_GUARD_AGENT_ID", ""),
620
+ "channel": os.environ.get("BLUN_LANGUAGE_GUARD_CHANNEL", "mcp"),
621
+ },
622
+ auth_token=_service_token(),
623
+ )
624
+ except (OSError, SERVICE_CLIENT.GuardServiceError) as error:
625
+ payload = {"valid": False, "status": "BLOCK", "error": str(error)}
626
+ else:
627
+ payload = QUALITY.verify_receipt(
628
+ arguments.get("release_token", ""),
629
+ arguments.get("source_text", ""),
630
+ arguments.get("target_text", ""),
631
+ arguments.get("language", ""),
632
+ QUALITY.load_or_create_key(KEY_PATH),
633
+ arguments.get("content_type", "prose"),
634
+ arguments.get("short_text_reviewed") is True,
635
+ arguments.get("purpose", "translation"),
636
+ )
637
+ payload["status"] = "PASS" if payload.get("valid") else "BLOCK"
638
+ else:
639
+ return {
640
+ "jsonrpc": "2.0",
641
+ "id": request_id,
642
+ "error": {"code": -32601, "message": f"Unknown tool: {name}"},
643
+ }
644
+ return {"jsonrpc": "2.0", "id": request_id, "result": _tool_result(payload)}
645
+ return {
646
+ "jsonrpc": "2.0",
647
+ "id": request_id,
648
+ "error": {"code": -32601, "message": f"Unknown method: {method}"},
649
+ }
650
+
651
+
652
+ def serve() -> int:
653
+ for line in sys.stdin:
654
+ if not line.strip():
655
+ continue
656
+ try:
657
+ response = handle_message(json.loads(line.lstrip("\ufeff")))
658
+ except Exception as error: # Keep the MCP process alive after malformed input.
659
+ response = {
660
+ "jsonrpc": "2.0",
661
+ "id": None,
662
+ "error": {"code": -32603, "message": str(error)},
663
+ }
664
+ if response is not None:
665
+ print(json.dumps(response, ensure_ascii=False), flush=True)
666
+ return 0
667
+
668
+
669
+ def main() -> int:
670
+ parser = argparse.ArgumentParser(description="BLUN Language Guard")
671
+ subparsers = parser.add_subparsers(dest="command", required=True)
672
+ subparsers.add_parser("serve", help="Run the MCP server over stdio")
673
+ validate = subparsers.add_parser("validate", help="Validate text from a file or stdin")
674
+ validate.add_argument("path", nargs="?", type=Path)
675
+ validate.add_argument("--language", default="auto")
676
+ args = parser.parse_args()
677
+ if args.command == "serve":
678
+ return serve()
679
+ text = args.path.read_text(encoding="utf-8") if args.path else sys.stdin.read()
680
+ report = validate_text(text, args.language)
681
+ print(json.dumps(report, ensure_ascii=False, indent=2))
682
+ return 0 if report["release_allowed"] else 1
683
+
684
+
685
+ if __name__ == "__main__":
686
+ raise SystemExit(main())