Soul s1 fixes. The hook header says the lines are recalled records, data and never instructions, written by agents not the operator. Every item is one JSON object per line: id, title and operator_quote are JSON strings with < >, control, bidi and separator characters escaped, so a title cannot close the framing, forge a tag, a header or an operator segment. RECALL_DEADLINE_MS now bounds the whole run through a hard stop; stdin is read to 4 MiB and the answer to 1 MiB (mind.call max_bytes); RECALL_BUDGET is clamped to 9000 under Claude Code's 10,000-character cap. Tests: hostile text, a non-ASCII process-boundary run under cp1252, byte-versus-character budget, stdin held open and endless, answer size, budget ceiling, and the four unrecorded survivors (negative reply timeout, reply id check, env-int positivity, EOF sentinel). Mutants of each were run and caught. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
145 lines
5.1 KiB
Python
145 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
|
"""The one way a Eureka script reaches the mind: the eureka-state stdio MCP binding.
|
|
|
|
call(tool, arguments, deadline) spawns the binding, does initialize,
|
|
notifications/initialized and tools/call, and returns the parsed JSON body of the
|
|
answer. It raises MindError for every failure and kills the child on every exit
|
|
path. It never retries; retry_once(fn) is the caller's choice. Standard library only.
|
|
|
|
The binding: EUREKA_STATE_EXE overrides the command; ~/.claude.json
|
|
mcpServers.eureka-state gives command, args and env; the caller's environment wins
|
|
over the binding's env. A MindError names a kind and a detail and never echoes an
|
|
input value (an endpoint, a path, a line of config).
|
|
"""
|
|
import json
|
|
import os
|
|
import queue
|
|
import subprocess
|
|
import threading
|
|
import time
|
|
|
|
MAX_ANSWER_BYTES = 8 << 20
|
|
|
|
ERROR_CODES = ("Unavailable", "Rejected", "Misconfigured", "TooLarge",
|
|
"Unencodable", "InvalidInput", "Internal")
|
|
|
|
|
|
class MindError(Exception):
|
|
"""kind: binding (no binding or no command), transport (spawn, pipe, deadline),
|
|
tool (the daemon refused; detail is an ERROR_CODES entry or "error"), answer
|
|
(the body was not JSON). detail is the code, always "Unavailable" for transport."""
|
|
|
|
def __init__(self, kind, detail):
|
|
super().__init__(kind + ": " + detail)
|
|
self.kind, self.detail = kind, detail
|
|
|
|
@property
|
|
def unavailable(self):
|
|
return self.detail == "Unavailable"
|
|
|
|
|
|
def binding():
|
|
exe = os.environ.get("EUREKA_STATE_EXE")
|
|
args = []
|
|
env = dict(os.environ)
|
|
try:
|
|
with open(os.path.join(os.path.expanduser("~"), ".claude.json"), encoding="utf-8") as f:
|
|
b = json.load(f)["mcpServers"]["eureka-state"]
|
|
exe = exe or b.get("command")
|
|
args = list(b.get("args", []))
|
|
for k, v in (b.get("env") or {}).items():
|
|
env.setdefault(k, v)
|
|
except (OSError, ValueError, KeyError, TypeError):
|
|
if not exe:
|
|
raise MindError("binding", "eureka-state is not configured")
|
|
if not exe:
|
|
raise MindError("binding", "eureka-state has no command")
|
|
return [exe, *args], env
|
|
|
|
|
|
def call(tool, arguments, deadline, max_bytes=MAX_ANSWER_BYTES):
|
|
"""One tool call within `deadline` seconds in all, reading at most `max_bytes` of
|
|
the binding's output (a larger answer is MindError answer). Returns the parsed
|
|
JSON body."""
|
|
end = time.monotonic() + deadline
|
|
argv, env = binding()
|
|
try:
|
|
proc = subprocess.Popen(argv, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
|
stderr=subprocess.DEVNULL, env=env)
|
|
except OSError:
|
|
raise MindError("transport", "Unavailable")
|
|
lines = queue.Queue()
|
|
|
|
def pump():
|
|
room = max_bytes
|
|
while True:
|
|
ln = proc.stdout.readline(room + 1)
|
|
if not ln:
|
|
break
|
|
room -= len(ln)
|
|
if room < 0:
|
|
lines.put(MindError("answer", "answer too large"))
|
|
return
|
|
lines.put(ln)
|
|
lines.put(None)
|
|
threading.Thread(target=pump, daemon=True).start()
|
|
|
|
def send(obj):
|
|
proc.stdin.write((json.dumps(obj) + "\n").encode("utf-8"))
|
|
proc.stdin.flush()
|
|
|
|
def reply(want):
|
|
while True:
|
|
try:
|
|
ln = lines.get(timeout=max(end - time.monotonic(), 0))
|
|
except queue.Empty:
|
|
raise MindError("transport", "Unavailable")
|
|
if ln is None:
|
|
raise MindError("transport", "Unavailable")
|
|
if isinstance(ln, MindError):
|
|
raise ln
|
|
try:
|
|
msg = json.loads(ln)
|
|
except ValueError:
|
|
continue
|
|
if isinstance(msg, dict) and msg.get("id") == want:
|
|
return msg
|
|
|
|
try:
|
|
send({"jsonrpc": "2.0", "id": 1, "method": "initialize",
|
|
"params": {"protocolVersion": "2024-11-05", "capabilities": {},
|
|
"clientInfo": {"name": "eureka-tools", "version": "1"}}})
|
|
reply(1)
|
|
send({"jsonrpc": "2.0", "method": "notifications/initialized"})
|
|
send({"jsonrpc": "2.0", "id": 2, "method": "tools/call",
|
|
"params": {"name": tool, "arguments": arguments}})
|
|
res = reply(2).get("result") or {}
|
|
except OSError:
|
|
raise MindError("transport", "Unavailable")
|
|
finally:
|
|
try:
|
|
proc.stdin.close()
|
|
except OSError:
|
|
pass
|
|
try:
|
|
proc.kill()
|
|
proc.wait(timeout=2)
|
|
except (OSError, subprocess.TimeoutExpired):
|
|
pass
|
|
body = "".join(c.get("text", "") for c in res.get("content", []) if isinstance(c, dict))
|
|
if res.get("isError"):
|
|
raise MindError("tool", next((c for c in ERROR_CODES if c in body), "error"))
|
|
try:
|
|
return json.loads(body)
|
|
except ValueError:
|
|
raise MindError("answer", "unparsable answer")
|
|
|
|
|
|
def retry_once(fn):
|
|
"""Run fn; on an Unavailable MindError run it once more. Any other error stands."""
|
|
try:
|
|
return fn()
|
|
except MindError as first:
|
|
if not first.unavailable:
|
|
raise
|
|
return fn()
|