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,264 +1,264 @@
1
- #!/usr/bin/env python3
2
- """Zero-dependency client for the isolated BLUN Language Guard service."""
3
-
4
- from __future__ import annotations
5
-
6
- import json
7
- import os
8
- import socket
9
- import stat
10
- from contextlib import contextmanager
11
- from pathlib import Path
12
- from typing import Any, Iterator
13
-
14
-
15
- MAX_RESPONSE_BYTES = 8 * 1024 * 1024
16
- MAX_SERVICE_TOKEN_BYTES = 64 * 1024
17
-
18
-
19
- class GuardServiceError(RuntimeError):
20
- """Raised when the isolated guard cannot evaluate a request safely."""
21
-
22
-
23
- def _token_file_identity(details: os.stat_result) -> tuple[int, int, int, int, int, int]:
24
- return (
25
- details.st_dev,
26
- details.st_ino,
27
- details.st_nlink,
28
- details.st_size,
29
- details.st_ctime_ns,
30
- details.st_mtime_ns,
31
- )
32
-
33
-
34
- def _token_directory_identity(
35
- details: os.stat_result,
36
- ) -> tuple[int, int, int, int, int]:
37
- return (
38
- details.st_dev,
39
- details.st_ino,
40
- details.st_mode,
41
- details.st_uid,
42
- details.st_gid,
43
- )
44
-
45
-
46
- def _validate_token_directory(path: Path, details: os.stat_result) -> None:
47
- if not stat.S_ISDIR(details.st_mode) or stat.S_ISLNK(details.st_mode):
48
- raise GuardServiceError(f"guard service token directory is invalid: {path}")
49
- if os.name != "nt" and stat.S_IMODE(details.st_mode) & 0o022:
50
- raise GuardServiceError(
51
- f"guard service token directory is writable outside its owner: {path}"
52
- )
53
- if hasattr(os, "getuid") and details.st_uid != os.getuid():
54
- raise GuardServiceError(f"guard service token directory has the wrong owner: {path}")
55
-
56
-
57
- def _existing_token_directory_anchor(path: Path) -> Path:
58
- candidate = path
59
- while True:
60
- try:
61
- candidate.lstat()
62
- return candidate
63
- except FileNotFoundError:
64
- parent = candidate.parent
65
- if parent == candidate:
66
- raise GuardServiceError(
67
- f"guard service token directory has no existing anchor: {path}"
68
- )
69
- candidate = parent
70
-
71
-
72
- def _assert_token_directory_unchanged(
73
- path: Path, expected: tuple[int, int, int, int, int],
74
- ) -> None:
75
- try:
76
- current = path.lstat()
77
- except OSError as error:
78
- raise GuardServiceError(
79
- f"guard service token directory cannot be rechecked: {path}"
80
- ) from error
81
- _validate_token_directory(path, current)
82
- if _token_directory_identity(current) != expected:
83
- raise GuardServiceError(
84
- f"guard service token directory changed during read: {path}"
85
- )
86
-
87
-
88
- @contextmanager
89
- def _open_token_directory(path: Path) -> Iterator[int | None]:
90
- if os.name == "nt":
91
- yield None
92
- return
93
- anchor = Path.home()
94
- try:
95
- relative = path.parent.relative_to(anchor)
96
- except ValueError:
97
- anchor = _existing_token_directory_anchor(path.parent)
98
- relative = path.parent.relative_to(anchor)
99
- flags = (
100
- os.O_RDONLY
101
- | getattr(os, "O_BINARY", 0)
102
- | getattr(os, "O_DIRECTORY", 0)
103
- | getattr(os, "O_NOFOLLOW", 0)
104
- )
105
- descriptor = None
106
- current_path = anchor
107
- try:
108
- try:
109
- descriptor = os.open(anchor, flags)
110
- _validate_token_directory(anchor, os.fstat(descriptor))
111
- for component in relative.parts:
112
- current_path = current_path / component
113
- child = os.open(component, flags, dir_fd=descriptor)
114
- try:
115
- _validate_token_directory(current_path, os.fstat(child))
116
- except Exception:
117
- os.close(child)
118
- raise
119
- os.close(descriptor)
120
- descriptor = child
121
- expected = _token_directory_identity(os.fstat(descriptor))
122
- except (FileNotFoundError, GuardServiceError):
123
- raise
124
- except OSError as error:
125
- raise GuardServiceError(
126
- f"guard service token directory cannot be opened safely: {current_path}"
127
- ) from error
128
- try:
129
- yield descriptor
130
- finally:
131
- _assert_token_directory_unchanged(path.parent, expected)
132
- finally:
133
- if descriptor is not None:
134
- os.close(descriptor)
135
-
136
-
137
- def _validate_token_file(details: os.stat_result) -> None:
138
- if not stat.S_ISREG(details.st_mode) or stat.S_ISLNK(details.st_mode):
139
- raise GuardServiceError("guard service token must be a regular file")
140
- if details.st_nlink != 1:
141
- raise GuardServiceError("guard service token must not have additional hard links")
142
- if details.st_size < 32 or details.st_size > MAX_SERVICE_TOKEN_BYTES:
143
- raise GuardServiceError("guard service token has an invalid size")
144
- if os.name != "nt" and stat.S_IMODE(details.st_mode) & 0o077:
145
- raise GuardServiceError("guard service token permissions must be owner-only")
146
- if hasattr(os, "getuid") and details.st_uid != os.getuid():
147
- raise GuardServiceError("guard service token has the wrong owner")
148
-
149
-
150
- def _token_lstat(path: Path, directory: int | None) -> os.stat_result:
151
- if directory is None:
152
- return os.lstat(path)
153
- return os.stat(path.name, dir_fd=directory, follow_symlinks=False)
154
-
155
-
156
- def _open_token_file(path: Path, directory: int | None) -> int:
157
- flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
158
- if directory is None:
159
- return os.open(path, flags)
160
- return os.open(path.name, flags, dir_fd=directory)
161
-
162
-
163
- def _token_fstat(descriptor: int) -> os.stat_result:
164
- return os.fstat(descriptor)
165
-
166
-
167
- def load_service_token(path: Path) -> str:
168
- """Read a bounded owner-only service token without following a replaced file."""
169
- with _open_token_directory(path) as directory:
170
- before = _token_lstat(path, directory)
171
- _validate_token_file(before)
172
- descriptor = _open_token_file(path, directory)
173
- try:
174
- opened = _token_fstat(descriptor)
175
- _validate_token_file(opened)
176
- if _token_file_identity(opened) != _token_file_identity(before):
177
- raise GuardServiceError("guard service token changed while opening")
178
- with os.fdopen(descriptor, "rb", closefd=False) as handle:
179
- raw = handle.read(MAX_SERVICE_TOKEN_BYTES + 1)
180
- after = _token_fstat(descriptor)
181
- after_path = _token_lstat(path, directory)
182
- if (
183
- _token_file_identity(after) != _token_file_identity(opened)
184
- or _token_file_identity(after_path) != _token_file_identity(opened)
185
- ):
186
- raise GuardServiceError("guard service token changed while reading")
187
- finally:
188
- os.close(descriptor)
189
- if len(raw) > MAX_SERVICE_TOKEN_BYTES:
190
- raise GuardServiceError("guard service token has an invalid size")
191
- try:
192
- token = raw.decode("utf-8-sig").strip()
193
- except UnicodeDecodeError as error:
194
- raise GuardServiceError("guard service token is not valid UTF-8") from error
195
- if len(token) < 32:
196
- raise GuardServiceError("guard service token is invalid")
197
- return token
198
-
199
-
200
- def parse_endpoint(endpoint: str) -> tuple[str, str | tuple[str, int]]:
201
- value = str(endpoint or "").strip()
202
- if value.startswith("unix:") and value[5:]:
203
- return "unix", value[5:]
204
- if value.startswith("tcp:"):
205
- host_port = value[4:]
206
- host, separator, raw_port = host_port.rpartition(":")
207
- if separator and host in {"127.0.0.1", "localhost", "::1"}:
208
- try:
209
- port = int(raw_port)
210
- except ValueError as error:
211
- raise GuardServiceError("invalid guard service TCP port") from error
212
- if 1 <= port <= 65535:
213
- return "tcp", (host, port)
214
- raise GuardServiceError("guard service endpoint must be unix:/path or loopback tcp:host:port")
215
-
216
-
217
- def call_guard_service(
218
- endpoint: str,
219
- request: dict[str, Any],
220
- *,
221
- auth_token: str = "",
222
- timeout: float = 10.0,
223
- ) -> dict[str, Any]:
224
- transport, address = parse_endpoint(endpoint)
225
- payload = dict(request)
226
- if auth_token:
227
- payload["service_token"] = auth_token
228
- encoded = (json.dumps(payload, ensure_ascii=False, separators=(",", ":")) + "\n").encode("utf-8")
229
- if len(encoded) > MAX_RESPONSE_BYTES:
230
- raise GuardServiceError("guard service request is too large")
231
- try:
232
- if transport == "unix":
233
- if not hasattr(socket, "AF_UNIX"):
234
- raise GuardServiceError("Unix sockets are unavailable on this platform")
235
- client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
236
- else:
237
- client = socket.create_connection(address, timeout=timeout)
238
- with client:
239
- client.settimeout(timeout)
240
- if transport == "unix":
241
- client.connect(address)
242
- client.sendall(encoded)
243
- chunks: list[bytes] = []
244
- size = 0
245
- while True:
246
- chunk = client.recv(65536)
247
- if not chunk:
248
- break
249
- chunks.append(chunk)
250
- size += len(chunk)
251
- if size > MAX_RESPONSE_BYTES:
252
- raise GuardServiceError("guard service response is too large")
253
- if b"\n" in chunk:
254
- break
255
- except (OSError, TimeoutError) as error:
256
- raise GuardServiceError("guard service is unavailable") from error
257
- raw = b"".join(chunks).split(b"\n", 1)[0]
258
- try:
259
- response = json.loads(raw.decode("utf-8"))
260
- except (UnicodeDecodeError, json.JSONDecodeError) as error:
261
- raise GuardServiceError("guard service returned an invalid response") from error
262
- if not isinstance(response, dict):
263
- raise GuardServiceError("guard service response must be an object")
264
- return response
1
+ #!/usr/bin/env python3
2
+ """Zero-dependency client for the isolated BLUN Language Guard service."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import os
8
+ import socket
9
+ import stat
10
+ from contextlib import contextmanager
11
+ from pathlib import Path
12
+ from typing import Any, Iterator
13
+
14
+
15
+ MAX_RESPONSE_BYTES = 8 * 1024 * 1024
16
+ MAX_SERVICE_TOKEN_BYTES = 64 * 1024
17
+
18
+
19
+ class GuardServiceError(RuntimeError):
20
+ """Raised when the isolated guard cannot evaluate a request safely."""
21
+
22
+
23
+ def _token_file_identity(details: os.stat_result) -> tuple[int, int, int, int, int, int]:
24
+ return (
25
+ details.st_dev,
26
+ details.st_ino,
27
+ details.st_nlink,
28
+ details.st_size,
29
+ details.st_ctime_ns,
30
+ details.st_mtime_ns,
31
+ )
32
+
33
+
34
+ def _token_directory_identity(
35
+ details: os.stat_result,
36
+ ) -> tuple[int, int, int, int, int]:
37
+ return (
38
+ details.st_dev,
39
+ details.st_ino,
40
+ details.st_mode,
41
+ details.st_uid,
42
+ details.st_gid,
43
+ )
44
+
45
+
46
+ def _validate_token_directory(path: Path, details: os.stat_result) -> None:
47
+ if not stat.S_ISDIR(details.st_mode) or stat.S_ISLNK(details.st_mode):
48
+ raise GuardServiceError(f"guard service token directory is invalid: {path}")
49
+ if os.name != "nt" and stat.S_IMODE(details.st_mode) & 0o022:
50
+ raise GuardServiceError(
51
+ f"guard service token directory is writable outside its owner: {path}"
52
+ )
53
+ if hasattr(os, "getuid") and details.st_uid != os.getuid():
54
+ raise GuardServiceError(f"guard service token directory has the wrong owner: {path}")
55
+
56
+
57
+ def _existing_token_directory_anchor(path: Path) -> Path:
58
+ candidate = path
59
+ while True:
60
+ try:
61
+ candidate.lstat()
62
+ return candidate
63
+ except FileNotFoundError:
64
+ parent = candidate.parent
65
+ if parent == candidate:
66
+ raise GuardServiceError(
67
+ f"guard service token directory has no existing anchor: {path}"
68
+ )
69
+ candidate = parent
70
+
71
+
72
+ def _assert_token_directory_unchanged(
73
+ path: Path, expected: tuple[int, int, int, int, int],
74
+ ) -> None:
75
+ try:
76
+ current = path.lstat()
77
+ except OSError as error:
78
+ raise GuardServiceError(
79
+ f"guard service token directory cannot be rechecked: {path}"
80
+ ) from error
81
+ _validate_token_directory(path, current)
82
+ if _token_directory_identity(current) != expected:
83
+ raise GuardServiceError(
84
+ f"guard service token directory changed during read: {path}"
85
+ )
86
+
87
+
88
+ @contextmanager
89
+ def _open_token_directory(path: Path) -> Iterator[int | None]:
90
+ if os.name == "nt":
91
+ yield None
92
+ return
93
+ anchor = Path.home()
94
+ try:
95
+ relative = path.parent.relative_to(anchor)
96
+ except ValueError:
97
+ anchor = _existing_token_directory_anchor(path.parent)
98
+ relative = path.parent.relative_to(anchor)
99
+ flags = (
100
+ os.O_RDONLY
101
+ | getattr(os, "O_BINARY", 0)
102
+ | getattr(os, "O_DIRECTORY", 0)
103
+ | getattr(os, "O_NOFOLLOW", 0)
104
+ )
105
+ descriptor = None
106
+ current_path = anchor
107
+ try:
108
+ try:
109
+ descriptor = os.open(anchor, flags)
110
+ _validate_token_directory(anchor, os.fstat(descriptor))
111
+ for component in relative.parts:
112
+ current_path = current_path / component
113
+ child = os.open(component, flags, dir_fd=descriptor)
114
+ try:
115
+ _validate_token_directory(current_path, os.fstat(child))
116
+ except Exception:
117
+ os.close(child)
118
+ raise
119
+ os.close(descriptor)
120
+ descriptor = child
121
+ expected = _token_directory_identity(os.fstat(descriptor))
122
+ except (FileNotFoundError, GuardServiceError):
123
+ raise
124
+ except OSError as error:
125
+ raise GuardServiceError(
126
+ f"guard service token directory cannot be opened safely: {current_path}"
127
+ ) from error
128
+ try:
129
+ yield descriptor
130
+ finally:
131
+ _assert_token_directory_unchanged(path.parent, expected)
132
+ finally:
133
+ if descriptor is not None:
134
+ os.close(descriptor)
135
+
136
+
137
+ def _validate_token_file(details: os.stat_result) -> None:
138
+ if not stat.S_ISREG(details.st_mode) or stat.S_ISLNK(details.st_mode):
139
+ raise GuardServiceError("guard service token must be a regular file")
140
+ if details.st_nlink != 1:
141
+ raise GuardServiceError("guard service token must not have additional hard links")
142
+ if details.st_size < 32 or details.st_size > MAX_SERVICE_TOKEN_BYTES:
143
+ raise GuardServiceError("guard service token has an invalid size")
144
+ if os.name != "nt" and stat.S_IMODE(details.st_mode) & 0o077:
145
+ raise GuardServiceError("guard service token permissions must be owner-only")
146
+ if hasattr(os, "getuid") and details.st_uid != os.getuid():
147
+ raise GuardServiceError("guard service token has the wrong owner")
148
+
149
+
150
+ def _token_lstat(path: Path, directory: int | None) -> os.stat_result:
151
+ if directory is None:
152
+ return os.lstat(path)
153
+ return os.stat(path.name, dir_fd=directory, follow_symlinks=False)
154
+
155
+
156
+ def _open_token_file(path: Path, directory: int | None) -> int:
157
+ flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
158
+ if directory is None:
159
+ return os.open(path, flags)
160
+ return os.open(path.name, flags, dir_fd=directory)
161
+
162
+
163
+ def _token_fstat(descriptor: int) -> os.stat_result:
164
+ return os.fstat(descriptor)
165
+
166
+
167
+ def load_service_token(path: Path) -> str:
168
+ """Read a bounded owner-only service token without following a replaced file."""
169
+ with _open_token_directory(path) as directory:
170
+ before = _token_lstat(path, directory)
171
+ _validate_token_file(before)
172
+ descriptor = _open_token_file(path, directory)
173
+ try:
174
+ opened = _token_fstat(descriptor)
175
+ _validate_token_file(opened)
176
+ if _token_file_identity(opened) != _token_file_identity(before):
177
+ raise GuardServiceError("guard service token changed while opening")
178
+ with os.fdopen(descriptor, "rb", closefd=False) as handle:
179
+ raw = handle.read(MAX_SERVICE_TOKEN_BYTES + 1)
180
+ after = _token_fstat(descriptor)
181
+ after_path = _token_lstat(path, directory)
182
+ if (
183
+ _token_file_identity(after) != _token_file_identity(opened)
184
+ or _token_file_identity(after_path) != _token_file_identity(opened)
185
+ ):
186
+ raise GuardServiceError("guard service token changed while reading")
187
+ finally:
188
+ os.close(descriptor)
189
+ if len(raw) > MAX_SERVICE_TOKEN_BYTES:
190
+ raise GuardServiceError("guard service token has an invalid size")
191
+ try:
192
+ token = raw.decode("utf-8-sig").strip()
193
+ except UnicodeDecodeError as error:
194
+ raise GuardServiceError("guard service token is not valid UTF-8") from error
195
+ if len(token) < 32:
196
+ raise GuardServiceError("guard service token is invalid")
197
+ return token
198
+
199
+
200
+ def parse_endpoint(endpoint: str) -> tuple[str, str | tuple[str, int]]:
201
+ value = str(endpoint or "").strip()
202
+ if value.startswith("unix:") and value[5:]:
203
+ return "unix", value[5:]
204
+ if value.startswith("tcp:"):
205
+ host_port = value[4:]
206
+ host, separator, raw_port = host_port.rpartition(":")
207
+ if separator and host in {"127.0.0.1", "localhost", "::1"}:
208
+ try:
209
+ port = int(raw_port)
210
+ except ValueError as error:
211
+ raise GuardServiceError("invalid guard service TCP port") from error
212
+ if 1 <= port <= 65535:
213
+ return "tcp", (host, port)
214
+ raise GuardServiceError("guard service endpoint must be unix:/path or loopback tcp:host:port")
215
+
216
+
217
+ def call_guard_service(
218
+ endpoint: str,
219
+ request: dict[str, Any],
220
+ *,
221
+ auth_token: str = "",
222
+ timeout: float = 10.0,
223
+ ) -> dict[str, Any]:
224
+ transport, address = parse_endpoint(endpoint)
225
+ payload = dict(request)
226
+ if auth_token:
227
+ payload["service_token"] = auth_token
228
+ encoded = (json.dumps(payload, ensure_ascii=False, separators=(",", ":")) + "\n").encode("utf-8")
229
+ if len(encoded) > MAX_RESPONSE_BYTES:
230
+ raise GuardServiceError("guard service request is too large")
231
+ try:
232
+ if transport == "unix":
233
+ if not hasattr(socket, "AF_UNIX"):
234
+ raise GuardServiceError("Unix sockets are unavailable on this platform")
235
+ client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
236
+ else:
237
+ client = socket.create_connection(address, timeout=timeout)
238
+ with client:
239
+ client.settimeout(timeout)
240
+ if transport == "unix":
241
+ client.connect(address)
242
+ client.sendall(encoded)
243
+ chunks: list[bytes] = []
244
+ size = 0
245
+ while True:
246
+ chunk = client.recv(65536)
247
+ if not chunk:
248
+ break
249
+ chunks.append(chunk)
250
+ size += len(chunk)
251
+ if size > MAX_RESPONSE_BYTES:
252
+ raise GuardServiceError("guard service response is too large")
253
+ if b"\n" in chunk:
254
+ break
255
+ except (OSError, TimeoutError) as error:
256
+ raise GuardServiceError("guard service is unavailable") from error
257
+ raw = b"".join(chunks).split(b"\n", 1)[0]
258
+ try:
259
+ response = json.loads(raw.decode("utf-8"))
260
+ except (UnicodeDecodeError, json.JSONDecodeError) as error:
261
+ raise GuardServiceError("guard service returned an invalid response") from error
262
+ if not isinstance(response, dict):
263
+ raise GuardServiceError("guard service response must be an object")
264
+ return response
@@ -1,62 +1,62 @@
1
- #!/usr/bin/env python3
2
- """Installed-skill entry point for the fail-closed BLUN Language Gateway."""
3
-
4
- from __future__ import annotations
5
-
6
- import argparse
7
- import importlib.util
8
- import json
9
- import sys
10
- from pathlib import Path
11
-
12
-
13
- GUARD_PATH = Path(__file__).with_name("blun_language_guard.py")
14
- SPEC = importlib.util.spec_from_file_location("blun_installed_gateway_guard", GUARD_PATH)
15
- assert SPEC and SPEC.loader
16
- GUARD = importlib.util.module_from_spec(SPEC)
17
- sys.modules[SPEC.name] = GUARD
18
- SPEC.loader.exec_module(GUARD)
19
-
20
-
21
- def gate(request: dict) -> dict:
22
- required = ("task_kind", "target_text", "language")
23
- missing = [
24
- key for key in required
25
- if not isinstance(request.get(key), str) or not request[key].strip()
26
- ]
27
- if missing:
28
- return {"status": "BLOCK", "release_allowed": False, "reason": "missing-fields", "fields": missing}
29
- task_kind = request["task_kind"]
30
- source = request.get("source_text", "")
31
- if not isinstance(source, str):
32
- return {"status": "BLOCK", "release_allowed": False, "reason": "invalid-source-type"}
33
- if task_kind == "translation":
34
- if not source.strip():
35
- return {"status": "BLOCK", "release_allowed": False, "reason": "translation-source-required"}
36
- result = GUARD.release_translation(request)
37
- elif task_kind == "response":
38
- if source.strip():
39
- return {"status": "BLOCK", "release_allowed": False, "reason": "response-cannot-carry-source"}
40
- result = GUARD.release_response(request)
41
- else:
42
- return {"status": "BLOCK", "release_allowed": False, "reason": "invalid-task-kind"}
43
- result["gateway"] = f"blun-language-gateway/{GUARD.VERSION}"
44
- result["task_kind"] = task_kind
45
- return result
46
-
47
-
48
- def main() -> int:
49
- parser = argparse.ArgumentParser(description="BLUN fail-closed language output gateway")
50
- parser.add_argument("--input", type=Path)
51
- args = parser.parse_args()
52
- try:
53
- request = json.loads(args.input.read_text(encoding="utf-8-sig") if args.input else sys.stdin.read().lstrip("\ufeff"))
54
- result = gate(request)
55
- except (OSError, json.JSONDecodeError) as error:
56
- result = {"status": "BLOCK", "release_allowed": False, "reason": "invalid-input", "error": str(error)}
57
- print(json.dumps(result, ensure_ascii=False, indent=2))
58
- return 0 if result.get("release_allowed") else 1
59
-
60
-
61
- if __name__ == "__main__":
62
- raise SystemExit(main())
1
+ #!/usr/bin/env python3
2
+ """Installed-skill entry point for the fail-closed BLUN Language Gateway."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import importlib.util
8
+ import json
9
+ import sys
10
+ from pathlib import Path
11
+
12
+
13
+ GUARD_PATH = Path(__file__).with_name("blun_language_guard.py")
14
+ SPEC = importlib.util.spec_from_file_location("blun_installed_gateway_guard", GUARD_PATH)
15
+ assert SPEC and SPEC.loader
16
+ GUARD = importlib.util.module_from_spec(SPEC)
17
+ sys.modules[SPEC.name] = GUARD
18
+ SPEC.loader.exec_module(GUARD)
19
+
20
+
21
+ def gate(request: dict) -> dict:
22
+ required = ("task_kind", "target_text", "language")
23
+ missing = [
24
+ key for key in required
25
+ if not isinstance(request.get(key), str) or not request[key].strip()
26
+ ]
27
+ if missing:
28
+ return {"status": "BLOCK", "release_allowed": False, "reason": "missing-fields", "fields": missing}
29
+ task_kind = request["task_kind"]
30
+ source = request.get("source_text", "")
31
+ if not isinstance(source, str):
32
+ return {"status": "BLOCK", "release_allowed": False, "reason": "invalid-source-type"}
33
+ if task_kind == "translation":
34
+ if not source.strip():
35
+ return {"status": "BLOCK", "release_allowed": False, "reason": "translation-source-required"}
36
+ result = GUARD.release_translation(request)
37
+ elif task_kind == "response":
38
+ if source.strip():
39
+ return {"status": "BLOCK", "release_allowed": False, "reason": "response-cannot-carry-source"}
40
+ result = GUARD.release_response(request)
41
+ else:
42
+ return {"status": "BLOCK", "release_allowed": False, "reason": "invalid-task-kind"}
43
+ result["gateway"] = f"blun-language-gateway/{GUARD.VERSION}"
44
+ result["task_kind"] = task_kind
45
+ return result
46
+
47
+
48
+ def main() -> int:
49
+ parser = argparse.ArgumentParser(description="BLUN fail-closed language output gateway")
50
+ parser.add_argument("--input", type=Path)
51
+ args = parser.parse_args()
52
+ try:
53
+ request = json.loads(args.input.read_text(encoding="utf-8-sig") if args.input else sys.stdin.read().lstrip("\ufeff"))
54
+ result = gate(request)
55
+ except (OSError, json.JSONDecodeError) as error:
56
+ result = {"status": "BLOCK", "release_allowed": False, "reason": "invalid-input", "error": str(error)}
57
+ print(json.dumps(result, ensure_ascii=False, indent=2))
58
+ return 0 if result.get("release_allowed") else 1
59
+
60
+
61
+ if __name__ == "__main__":
62
+ raise SystemExit(main())