merge: ae8675943507c4843

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 06:44:27 +01:00
co-authored by Claude Sonnet 5.5
114 changed files with 17362 additions and 4 deletions
+30
View File
@@ -0,0 +1,30 @@
// capmcp_cli: a thin test driver around the CAP and MCP classes. It stands in
// for main.cpp (which the integrator owns) so the same argv can be run through
// tai.py and through the C++ classes and the outputs diffed.
//
// capmcp_cli --cap-manifest
// capmcp_cli --cap COMMAND ...
#include <cstdio>
#include <string>
#include <vector>
#include "cap_agent_manifest.h"
#include "cap_cli.h"
int main(int argc, char** argv) {
std::vector<std::string> args(argv + 1, argv + argc);
if (args.empty()) {
std::fputs("usage: capmcp_cli --cap-manifest | --cap COMMAND ...\n", stderr);
return 2;
}
if (args[0] == "--cap-manifest") {
// print(TAI_CAP_MANIFEST)
std::fputs((detz::cap::CapAgentManifest::TAI_CAP_MANIFEST() + "\n").c_str(), stdout);
return 0;
}
if (args[0] == "--cap") {
return detz::cap::CapCli::capCliMain(std::vector<std::string>(args.begin() + 1, args.end()));
}
std::fputs("unknown mode\n", stderr);
return 2;
}
+326
View File
@@ -0,0 +1,326 @@
#!/usr/bin/env python3
"""Runs the same `--cap-manifest` / `--cap ...` argv through tai.py and through
the C++ test driver (build/capmcp/capmcp_cli) and diffs stdout, stderr and the
exit code. Every case runs in a throwaway HOME / TAI_HOME / cwd.
python3 -I tests/capmcp_cli_diff.py CPP_DRIVER [--tai PATH_TO_TAI_PY] [--filter TEXT] [--verbose]
Exit 0 only when all cases are byte-identical."""
import http.server
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
import textwrap
import threading
HERE = os.path.dirname(os.path.abspath(__file__))
TAI = os.path.join(os.path.dirname(os.path.dirname(HERE)), "tai.py")
FAKE_STREAM_AGENT = r'''
import json, sys
print(json.dumps({"type": "system", "subtype": "init", "session_id": "fake-1"}), flush=True)
n = 0
for line in sys.stdin:
try:
frame = json.loads(line)
except ValueError:
continue
n += 1
text = frame.get("message")
print(json.dumps({"type": "assistant", "message": {"id": "m%d" % n, "role": "assistant", "content": [
{"type": "text", "text": "echo %d: %s" % (n, text)},
{"type": "tool_use", "id": "t%d" % n, "name": "Read", "input": {"path": "x"}}]}}), flush=True)
print(json.dumps({"type": "user", "message": {"role": "user", "content": [
{"type": "tool_result", "tool_use_id": "t%d" % n, "content": "ok", "is_error": False}]}}), flush=True)
print(json.dumps({"type": "result", "subtype": "success", "usage": {"input_tokens": 3, "output_tokens": 5}, "duration_ms": 7, "total_cost_usd": 0.5}), flush=True)
'''
FAKE_ACP_AGENT = r'''
import json, sys
def send(obj):
print(json.dumps(obj), flush=True)
for line in sys.stdin:
msg = json.loads(line)
if "method" in msg and "id" in msg and msg["method"] == "initialize":
send({"jsonrpc": "2.0", "id": msg["id"], "result": {"protocolVersion": 1, "agentCapabilities": {}}})
elif msg.get("method") == "session/new":
send({"jsonrpc": "2.0", "id": msg["id"], "result": {"sessionId": "s1"}})
elif msg.get("method") == "session/prompt":
text = msg["params"]["prompt"][0]["text"]
send({"jsonrpc": "2.0", "method": "session/update", "params": {"sessionId": "s1", "update": {"sessionUpdate": "agent_message_chunk", "content": {"type": "text", "text": "acp says " + text}}}})
send({"jsonrpc": "2.0", "method": "session/update", "params": {"sessionId": "s1", "update": {"sessionUpdate": "tool_call", "toolCallId": "c1", "title": "Read file"}}})
send({"jsonrpc": "2.0", "method": "session/update", "params": {"sessionId": "s1", "update": {"sessionUpdate": "tool_call_update", "toolCallId": "c1", "status": "completed", "content": [{"type": "text", "text": "done"}]}}})
send({"jsonrpc": "2.0", "method": "session/update", "params": {"sessionId": "s1", "update": {"sessionUpdate": "usage_update", "usage": {"inputTokens": 10, "outputTokens": 4}}}})
send({"jsonrpc": "2.0", "id": msg["id"], "result": {"stopReason": "end_turn"}})
'''
FAKE_PTY_AGENT = r'''
import sys
sys.stdout.write("fake agent ready\nfake> ")
sys.stdout.flush()
for line in sys.stdin:
text = line.strip()
sys.stdout.write("you said: %s\n" % text)
sys.stdout.write("fake> ")
sys.stdout.flush()
'''
MANIFEST_STREAM = '''[agent]
name = "fake"
binary = "fake"
profiles = ["chat"]
[startup]
command = ["python3", "-c", "pass"]
ready_when = { pattern = "ready" }
[fast_path]
stream_json = ["python3", "{agent}"]
[pty]
cols = 80
rows = 24
[capabilities]
streaming_output = true
'''
MANIFEST_ACP = '''[agent]
name = "acpfake"
binary = "acpfake"
profiles = ["chat"]
[startup]
command = ["python3", "-c", "pass"]
ready_when = { pattern = "ready" }
[fast_path]
acp_stdio = true
[pty]
cols = 80
rows = 24
[capabilities]
streaming_output = true
'''
MANIFEST_PTY = '''[agent]
name = "ptyfake"
binary = "ptyfake"
profiles = ["chat"]
[startup]
command = ["python3", "{agent}"]
ready_when = { pattern = "fake agent ready" }
init_timeout_seconds = 20
[pty]
cols = 100
rows = 30
[parse]
idle = ["^fake> $"]
[capabilities]
streaming_output = true
'''
MANIFEST_A2A = '''[agent]
name = "a2afake"
binary = "a2afake"
profiles = ["chat"]
[startup]
command = ["python3", "-c", "pass"]
ready_when = { pattern = "ready" }
[fast_path]
a2a_serve_at = "http://127.0.0.1:{port}"
[pty]
cols = 80
rows = 24
[capabilities]
streaming_output = true
'''
FLEET_TOML = '''[fleet]
start = "cap://a"
task = "begin"
budget_usd = 100
[[sessions]]
name = "a"
agent = "fake"
[[sessions]]
name = "b"
agent = "fake"
[[routes]]
when = "cap://a.done"
route_to = "cap://b"
'''
class A2aHandler(http.server.BaseHTTPRequestHandler):
def log_message(self, *args):
pass
def do_GET(self):
if self.path == "/.well-known/agent-card.json":
body = json.dumps({"name": "a2a-card", "extensions": [{"uri": "cap-protocol/v1"}]}).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
else:
self.send_response(404)
self.send_header("Content-Length", "0")
self.end_headers()
def do_POST(self):
length = int(self.headers.get("Content-Length") or 0)
raw = self.rfile.read(length)
if self.path != "/message/send":
self.send_response(404)
self.send_header("Content-Length", "0")
self.end_headers()
return
data = json.loads(raw)
text = data["message"]["parts"][0]["text"]
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.end_headers()
frames = [
{"task": {"id": "t1", "contextId": "c1", "status": {"state": "working"}}},
{"message": {"messageId": "mm1", "parts": [{"kind": "text", "text": "a2a heard " + text}]}},
{"task": {"id": "t1", "contextId": "c1", "status": {"state": "completed"}}},
]
for frame in frames:
self.wfile.write(("data: " + json.dumps(frame) + "\n\n").encode())
self.wfile.flush()
def normalize(text, root):
text = text.replace(root, "<ROOT>")
text = re.sub(r"cap_[0-9a-f]{12}", "cap_<ID>", text)
return text
def run(argv, env, cwd, timeout=60, stdin=None):
done = subprocess.run(argv, cwd=cwd, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=timeout,
stdin=subprocess.DEVNULL if stdin is None else None, input=stdin)
return done.returncode, done.stdout.decode("utf-8", "replace"), done.stderr.decode("utf-8", "replace")
def main():
args = sys.argv[1:]
if not args:
print(__doc__)
return 2
cpp = os.path.abspath(args[0])
tai = TAI
if "--tai" in args:
tai = args[args.index("--tai") + 1]
only = args[args.index("--filter") + 1] if "--filter" in args else None
root = tempfile.mkdtemp(prefix="capmcp-cli-")
server = http.server.HTTPServer(("127.0.0.1", 0), A2aHandler)
port = server.server_address[1]
threading.Thread(target=server.serve_forever, daemon=True).start()
try:
home = os.path.join(root, "home")
agents = os.path.join(home, ".config", "cap", "agents")
os.makedirs(agents)
work = os.path.join(root, "work")
os.makedirs(work)
files = {
"fake_stream.py": FAKE_STREAM_AGENT, "fake_acp.py": FAKE_ACP_AGENT, "fake_pty.py": FAKE_PTY_AGENT,
}
for name, text in files.items():
with open(os.path.join(root, name), "w") as handle:
handle.write(text)
manifests = {
"fake.toml": MANIFEST_STREAM.replace("{agent}", os.path.join(root, "fake_stream.py")),
"acpfake.toml": MANIFEST_ACP,
"ptyfake.toml": MANIFEST_PTY.replace("{agent}", os.path.join(root, "fake_pty.py")),
"a2afake.toml": MANIFEST_A2A.replace("{port}", str(port)),
"broken.toml": "[agent]\nname = \"broken\"\n",
"wrongname.toml": MANIFEST_STREAM.replace('name = "fake"', 'name = "other"').replace("{agent}", "x"),
"badre.toml": MANIFEST_STREAM.replace('name = "fake"', 'name = "badre"').replace("{agent}", "x") + "\n[parse]\nidle = [\"(?<=a)b\"]\n",
"notoml.toml": "this is not toml = = =\n",
}
for name, text in manifests.items():
with open(os.path.join(agents, name), "w") as handle:
handle.write(text)
fleet = os.path.join(root, "fleet.toml")
with open(fleet, "w") as handle:
handle.write(FLEET_TOML)
badfleet = os.path.join(root, "badfleet.toml")
with open(badfleet, "w") as handle:
handle.write("[fleet]\nstart = \"cap://zzz\"\n[[sessions]]\nname = \"a\"\nagent = \"fake\"\n")
nofleet = os.path.join(root, "nofleet.toml")
with open(nofleet, "w") as handle:
handle.write("[fleet]\n")
garbage = os.path.join(root, "garbage.toml")
with open(garbage, "w") as handle:
handle.write("a = = 1")
env = dict(os.environ, HOME=home, TAI_HOME=os.path.join(root, "taihome"), COLUMNS="80", PYTHONDONTWRITEBYTECODE="1")
env.pop("TAI_PASSPHRASE", None)
cases = [
["--cap"], ["--cap", "help"], ["--cap", "-h"], ["--cap", "--help"], ["--cap", "bogus"], ["--cap", "resolve"],
["--cap", "resolve", "-h"], ["--cap", "resolve", "nosuchagent"], ["--cap", "resolve", "fake"],
["--cap", "resolve", "acpfake"], ["--cap", "resolve", "ptyfake"], ["--cap", "resolve", "a2afake"],
["--cap", "resolve", "broken"], ["--cap", "resolve", "wrongname"], ["--cap", "resolve", "badre"],
["--cap", "resolve", "notoml"], ["--cap", "resolve", "x", "--binary"], ["--cap", "resolve", "x", "y"],
["--cap", "chat"], ["--cap", "chat", "-h"], ["--cap", "chat", "--driver", "fake", "--max-risk", "bad"],
["--cap", "chat", "--driver", "nosuch"], ["--cap", "chat", "--driver", "fake", "--a", "x"],
["--cap", "run"], ["--cap", "run", "-h"], ["--cap", "run", os.path.join(root, "missing.toml")],
["--cap", "run", garbage], ["--cap", "run", nofleet], ["--cap", "run", badfleet],
["--cap", "run", fleet, "--task", " "],
["--cap", "chat", "--driver", "fake", "--task", "hello world"],
["--cap", "chat", "--driver", "ptyfake", "--task", "hello pty"],
["--cap", "chat", "--driver", "acpfake", "--argv", "python3 %s" % os.path.join(root, "fake_acp.py"), "--task", "hi acp"],
["--cap", "chat", "--driver", "acpfake", "--task", "no argv"],
["--cap", "chat", "--driver", "acpfake", "--argv", "python3 'unterminated", "--task", "x"],
["--cap", "chat", "--driver", "a2afake", "--task", "hi a2a"],
["--cap", "run", fleet],
["--cap", "run", fleet, "--task", "override task"],
]
for columns in ("40", "60", "100", "132"):
for sub in ("resolve", "chat", "run"):
cases.append(("COLUMNS=" + columns, ["--cap", sub, "-h"]))
cases.append(("COLUMNS=" + columns, ["--cap", sub, "--bogus-flag"]))
failures = 0
total = 0
for case in cases:
extra = {}
if isinstance(case, tuple):
key, value = case[0].split("=", 1)
extra[key] = value
case = case[1]
if only and only not in " ".join(case):
continue
total += 1
cenv = dict(env, **extra)
want = run([sys.executable, "-I", tai] + case, cenv, work)
got = run([cpp] + case, cenv, work)
want = (want[0], normalize(want[1], root), normalize(want[2], root))
got = (got[0], normalize(got[1], root), normalize(got[2], root))
if "--verbose" in args:
print("CASE %s -> rc %s stdout %r stderr %r" % (" ".join(case).replace(root, "<ROOT>"), want[0], want[1][:200], want[2][:160]))
if want != got:
failures += 1
print("MISMATCH %s" % " ".join(case).replace(root, "<ROOT>"))
for label, a, b in (("rc", want[0], got[0]), ("stdout", want[1], got[1]), ("stderr", want[2], got[2])):
if a != b:
print(" %s py : %r" % (label, a if label == "rc" else a[:600]))
print(" %s cpp: %r" % (label, b if label == "rc" else b[:600]))
# worktree dirs created by coding profiles must not leak between cases
shutil.rmtree(os.path.join(work, ".cap-worktrees"), ignore_errors=True)
want = run([sys.executable, "-I", tai, "--cap-manifest"], env, work)
got = run([cpp, "--cap-manifest"], env, work)
total += 1
if want != got:
failures += 1
print("MISMATCH --cap-manifest")
print("%d cases, %d mismatches" % (total, failures))
return 1 if failures else 0
finally:
server.shutdown()
shutil.rmtree(root, ignore_errors=True)
if __name__ == "__main__":
sys.exit(main())
+182
View File
@@ -0,0 +1,182 @@
#!/usr/bin/env python3
"""Differential test of the C++ wire layer (JSON, TOML, str(), float repr,
regex) against CPython. Builds nothing: pass the compiled probe.
python3 -I tests/capmcp_diff.py PROBE_BINARY [--seed N] [--fuzz N]
Exit 0 only when every case prints the same line in both programs."""
import json
import os
import random
import re
import subprocess
import sys
import tempfile
import tomllib
JSON_CASES = [
"", " ", "x", "{", "{\"a\"", "{\"a\":", "{\"a\":1", "{\"a\":1,", "{\"a\":1,}", "[1,]", "[1", "[", "[1 2]",
"{\"a\" 1}", "{a:1}", "{'a':1}", "\"abc", "\"a\\x\"", "\"a\\u12\"", "\"a\nb\"", "01", "-", "-x", "1.", "1e",
"1.5e3", "nul", "tru", "NaN", "-Infinity", "Infinity", "{\"a\":1} x", "[] []", "\"\\ud83d\\ude00\"",
"123456789012345678901234567890", " {\"a\": [1, 2.5, \"x\", null, true, false]} ", "{\"a\":1,\"a\":2}",
"\"\\u00e9\"", "[1,\n2,\n x]", "\ufeff{}", "{\"a\":1 \"b\":2}", "{,}", "[,1]", "{\"a\":}", "\"\\", "\"\\u",
"\"\\u12", "{\"a\":1,,}", "tru e", "-0", "-0.0", "1E5", "1e400", "-1e400", "\"é\"", "\"日本語\"", "[\"😀\"]",
"{\"b\":1,\"a\":{\"z\":[],\"y\":{}}}", "0.1", "1e-7", "1e22", "123456789012345680000.0", "5e-324", "1.7976931348623157e308",
"9223372036854775807", "9223372036854775808", "-9223372036854775808", "-9223372036854775809",
"\"\\/\\b\\f\\n\\r\\t\\\"\\\\\"", "\"\x7f\"", "\"\\u007f\"", "\"\\u2028\"", "{\"a\":\n\n [\n1\n,\n2] }",
]
TOML_CASES = [
"", "a = 1", "a = 1\nb = 2.5\nc = \"x\"\nd = true\ne = [1, 2, 3]\nf = { g = 1, h = \"i\" }",
"[t]\na = 1\n[t.u]\nb = 2", "[[s]]\nname = \"a\"\n[[s]]\nname = \"b\"",
"a = ", "a", "= 1", "a = 1 b = 2", "a = 1\na = 2", "[t]\n[t]", "a = [1, 2", "a = {x = 1", "a = \"abc",
"a = 'abc'\nb = '''multi\nline'''\nc = \"\"\"x\ny\"\"\"", "a = \"\"\"line \\\n continued\"\"\"",
"a = 0x1F\nb = 0o17\nc = 0b101\nd = 1_000\ne = +5\nf = -7", "a = 1e3\nb = 1.5e-3\nc = inf\nd = -inf\ne = nan",
"a = 01", "a = 1__0", "a = 0x", "a = .5", "a = 5.", "a = 1979-05-27", "a = 1979-05-27T07:32:00Z",
"a = 1979-05-27 07:32:00.5-08:00", "a = 07:32:00", "a = 1979-02-30", "a = 2024-02-29",
"a.b.c = 1\na.b.d = 2", "a.b = 1\n[a]\nc = 2", "[a]\nb = 1\n[a.b]", "[a.b]\nc = 1\n[a]\nd = 2",
"a = [[1], [2, 3], []]", "a = [\n 1, # one\n 2,\n]\n", "a = { b = 1, }", "a = [ ,]",
"# comment only\n", "a = 1 # trailing\n", "a = \"x\" y", "\"quoted key\" = 1\n'lit key' = 2",
"a = \"\\u00e9\\U0001F600\"", "a = \"\\x\"", "a = \"\\ud800\"", "a = \"tab\there\"", "a = \"nl\nx\"",
"[ a . b ]\nc = 1", "[a]\nb = 1\n[[a.c]]\nd = 1\n[[a.c]]\nd = 2", "x = {a.b = 1, a.c = 2}",
"x = {a = 1}\n[x.b]", "a = [1, \"two\", 3.0]", "[a]\n[a.b]\n[a]", "a = 1\n[a]", "[a]\nb = 1\n[a]\nc = 2",
"\r\na = 1\r\nb = 2\r\n", "a = 1\rb = 2", "a = \"\"\"\"x\"\"\"\"", "a = '''x'''''", "a=\"\"\"\\ \n x\"\"\"",
"a = tru", "a = True", "[]", "[a", "[[a]", "[[a]]\n[a]", "[a]\n[[a]]", "a = [\n", "key = 'unterminated",
"bad key = 1", "a = \"\x01\"", "# \x01", "a = 1_000.000_1", "a = 1e+3_0", "a = 0e0", "a = -0.0",
"a = 123456789012345678", "a = 9223372036854775807", "a = 9223372036854775808",
"[agent]\nname = \"tai\"\nprofiles = [\"coding\"]\n[probe]\ncommand = [\"tai.py\", \"--version\"]\nversion_pattern = \"tai (\\\\d+)\"",
]
REGEX_CASES = [
("^tai\\[.*\\]› $", "tai[main]› "), ("^tai\\[.*\\]› $", "tai[main]›"), ("^>\\s*$", "> "), ("^❯\\s*$", "❯"),
("(?P<tool>\\w+)\\((?P<args>.*)\\)", "run(ls -l)"), ("(?P<status>ok|failed)?x", "x"),
("github\\.com/([^/\\s?#]+)/([^/\\s?#]+?)(?:\\.git)?(?:[/?#]|$)", "https://github.com/o/r.git"),
("github\\.com/([^/\\s?#]+)/([^/\\s?#]+?)(?:\\.git)?(?:[/?#]|$)", "https://github.com/o/r/tree/x"),
("(npx|uvx)\\s+([^\\s\"'`]+)", "run: npx -y pkg-name now"), ("@(cap://[A-Za-z0-9_.-]+)", "hi @cap://a-1 and @cap://b"),
("\\$\\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\\}", "x ${FOO:-bar} y"),
("\\$\\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\\}", "x ${FOO} y"),
("resource_metadata=\"([^\"]+)\"", "Bearer resource_metadata=\"https://x/y\", realm=x"),
("\\bno\\b", "say no way"), ("\\byes\\b", "yes."), ("seal error:", "seal error: boom"),
("^seal error:", "x\nseal error:"), ("end$", "the end\n"), ("end$", "the end\nmore"), ("a{2}", "caab aab"),
("a{,2}b", "aab"), ("x{", "x{"), ("}", "a}"), ("[]a]+", "]]a"), ("[^]a]+", "bc]"), ("\\d+\\.\\d+", "v 1.44.2"),
("tai (\\d+\\.\\d+\\.\\d+)", "tai 1.44.2"), ("(a)|(b)", "b"), ("é+", "ééé"), ("\\w+", "日本語 x"),
("(?:ab)+", "ababab"), ("a(?=b)", "ab"), ("a(?!b)", "ac"), ("\\Aab", "ab"), ("ab\\Z", "ab"),
]
REPR_CASES = [
"{\"a\":\"it's\",\"b\":[1,null,true,1.5,\"x\\n\"]}", "[\"a\\\"b'c\"]", "[\"\\u00e9\\u2028\\u00a0\\u0007\"]",
"{}", "[]", "[1e22, 1e-7, 0.1, 100.0, 1e16, 123456.789]", "\"plain\"", "null", "[true,false]", "{\"k\":{\"n\":[{}]}}",
"\"日本語\"", "\"\\ud83d\\ude00\"", "[NaN, Infinity, -Infinity]", "\"\\\\\"",
]
FLOAT_CASES = ["0", "1", "0.1", "1e22", "1e-7", "100", "1e16", "1e15", "123456789.123456789", "5e-324", "1.7976931348623157e308",
"0.0001", "0.00001", "12345678901234567890", "-2.5", "3.14159", "1e300", "2.5e-5", "0.30000000000000004"]
def write_cases(path, cases):
with open(path, "wb") as handle:
handle.write(b"\0".join(c.encode("utf-8", "surrogatepass") for c in cases))
def py_json(i, text):
try:
return "%d OK %s" % (i, json.dumps(json.loads(text)))
except ValueError as exc:
return "%d ERR %s" % (i, exc)
def py_toml(i, text):
try:
return "%d OK %s" % (i, json.dumps(tomllib.loads(text), default=str))
except ValueError as exc:
return "%d ERR %s" % (i, exc)
def py_repr(i, text):
try:
return "%d OK %s" % (i, repr(json.loads(text)))
except ValueError as exc:
return "%d ERR %s" % (i, exc)
def py_float(i, text):
return "%d OK %s" % (i, repr(float(text)))
def py_regex(i, text):
pattern, subject = text.split("\x01", 1)
try:
found = re.search(pattern, subject)
except re.error as exc:
return "%d ERR %s" % (i, exc)
if not found:
return "%d NONE" % i
groups = [found.group(0)] + list(found.groups())
return "%d OK %d %d %s %s" % (i, found.start(), found.end(), json.dumps(groups), json.dumps(found.groupdict()))
def fuzz_json(rng, count):
out = []
atoms = ["null", "true", "false", "0", "-1", "12", "3.5", "-2.5e3", "\"s\"", "\"\\u00e9\"", "\"a b\"", "[]", "{}", "NaN", "\"\\n\""]
for _ in range(count):
depth = rng.randint(0, 3)
def build(d):
if d == 0:
return rng.choice(atoms)
if rng.random() < 0.5:
return "[" + ", ".join(build(d - 1) for _ in range(rng.randint(0, 3))) + "]"
return "{" + ", ".join("\"k%d\": %s" % (rng.randint(0, 5), build(d - 1)) for _ in range(rng.randint(0, 3))) + "}"
text = build(depth)
out.append(text)
if text:
cut = rng.randint(0, len(text))
out.append(text[:cut])
pos = rng.randint(0, len(text) - 1) if len(text) > 1 else 0
out.append(text[:pos] + rng.choice([",", "]", "}", "\"", "\\", " ", "x", ":"]) + text[pos + 1:])
return out
def run(probe, mode, cases, pyfn, label):
with tempfile.TemporaryDirectory() as tmp:
path = os.path.join(tmp, "cases.bin")
write_cases(path, cases)
done = subprocess.run([probe, mode, path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=120)
got = done.stdout.decode("utf-8", "replace").splitlines()
want = [pyfn(i, c) for i, c in enumerate(cases)]
bad = 0
for i, (g, w) in enumerate(zip(got, want)):
if g != w:
bad += 1
if bad <= 8:
print("MISMATCH %s case %d %r\n cpp: %s\n py : %s" % (label, i, cases[i][:120], g[:300], w[:300]))
if len(got) != len(want):
bad += 1
print("MISMATCH %s: %d lines from probe, %d cases (stderr: %s)" % (label, len(got), len(want), done.stderr.decode()[:300]))
print("%-8s %4d cases, %d mismatches" % (label, len(cases), bad))
return bad
def main():
args = sys.argv[1:]
if not args:
print(__doc__)
return 2
probe = args[0]
seed = int(args[args.index("--seed") + 1]) if "--seed" in args else 7
fuzz = int(args[args.index("--fuzz") + 1]) if "--fuzz" in args else 300
rng = random.Random(seed)
bad = 0
jcases = JSON_CASES + fuzz_json(rng, fuzz)
bad += run(probe, "json", jcases, py_json, "json")
bad += run(probe, "repr", REPR_CASES, py_repr, "repr")
bad += run(probe, "float", FLOAT_CASES, py_float, "float")
bad += run(probe, "toml", TOML_CASES, py_toml, "toml")
bad += run(probe, "regex", ["%s\x01%s" % c for c in REGEX_CASES], py_regex, "regex")
print("TOTAL mismatches: %d" % bad)
return 1 if bad else 0
if __name__ == "__main__":
sys.exit(main())
+72
View File
@@ -0,0 +1,72 @@
// capmcp_probe: differential probe for the wire layer. Reads NUL separated
// cases from a file and prints one result line per case; tests/capmcp_diff.py
// runs the same cases through CPython and diffs the two outputs.
//
// capmcp_probe json CASES json.loads + json.dumps
// capmcp_probe toml CASES tomllib.loads + json.dumps
// capmcp_probe repr CASES json.loads + str() of the value
// capmcp_probe regex CASES PATTERN\x01TEXT -> re.search groups
// capmcp_probe float CASES text -> repr(float(text))
#include <cstdio>
#include <fstream>
#include <iostream>
#include <iterator>
#include <sstream>
#include "wire_json.h"
#include "wire_regex.h"
#include "wire_text.h"
#include "wire_toml.h"
using detz::wire::Json;
using detz::wire::WireText;
static std::vector<std::string> loadCases(const char* path) {
std::ifstream in(path, std::ios::binary);
std::string all((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
std::vector<std::string> out;
size_t pos = 0;
while (pos <= all.size()) {
size_t at = all.find('\0', pos);
if (at == std::string::npos) {
if (pos < all.size()) out.push_back(all.substr(pos));
break;
}
out.push_back(all.substr(pos, at - pos));
pos = at + 1;
}
return out;
}
int main(int argc, char** argv) {
if (argc < 3) return 2;
const std::string mode = argv[1];
const auto cases = loadCases(argv[2]);
for (size_t i = 0; i < cases.size(); ++i) {
try {
if (mode == "json") {
std::printf("%zu OK %s\n", i, Json::parse(cases[i]).dumps().c_str());
} else if (mode == "toml") {
std::printf("%zu OK %s\n", i, detz::wire::WireToml::loads(cases[i]).dumps().c_str());
} else if (mode == "repr") {
std::printf("%zu OK %s\n", i, Json::parse(cases[i]).pyRepr().c_str());
} else if (mode == "float") {
std::printf("%zu OK %s\n", i, WireText::reprFloat(std::strtod(cases[i].c_str(), nullptr)).c_str());
} else if (mode == "regex") {
const size_t sep = cases[i].find('\x01');
detz::wire::WireRegex re(cases[i].substr(0, sep));
auto m = re.search(cases[i].substr(sep + 1));
if (!m) {
std::printf("%zu NONE\n", i);
} else {
Json groups = Json::array();
for (size_t g = 0; g <= m.groupCount(); ++g) groups.push(m.hasGroup(g) ? Json(m.group(g)) : Json());
std::printf("%zu OK %zu %zu %s %s\n", i, m.start(), m.end(), groups.dumps().c_str(), m.groupdict().dumps().c_str());
}
}
} catch (const std::exception& exc) {
std::printf("%zu ERR %s\n", i, exc.what());
}
}
return 0;
}
+196
View File
@@ -0,0 +1,196 @@
// capmcp_unit: table checks, drift checks against ../cap.md and ../mcp.md,
// the MCP vault tables, the normalizer and the served endpoint (in process).
// Run from taicpp/: build/capmcp/capmcp_unit
#include <sqlite3.h>
#include <cstdio>
#include <fstream>
#include <iterator>
#include <sstream>
#include "cap_agent_manifest.h"
#include "cap_binding.h"
#include "cap_constants.h"
#include "cap_error.h"
#include "cap_event.h"
#include "cap_manifest.h"
#include "cap_stream_json.h"
#include "mcp_constants.h"
#include "mcp_error.h"
#include "mcp_normalizer.h"
#include "mcp_serve_handler.h"
#include "mcp_sqlite_store.h"
using namespace detz;
using detz::wire::Json;
static int failures = 0;
#define CHECK(cond) \
do { \
if (!(cond)) { \
std::printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); \
++failures; \
} \
} while (0)
static std::string slurp(const char* path) {
std::ifstream in(path, std::ios::binary);
return std::string(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
}
class FakeAgent : public mcp::McpServeAgent {
public:
std::string dispatch(const std::string& name, const Json& args) override {
if (name == "echo") return args.dumps();
if (name == "deny") throw mcp::McpToolDenied();
return "error: unknown tool " + name;
}
std::string runTurn(const std::string& message, int steps, double) override {
return "reply:" + message + ":" + std::to_string(steps);
}
};
class FakeBackend : public mcp::McpServeBackend {
public:
std::vector<Json> toolSchemas() override {
return {Json::object({{"type", Json("function")},
{"function", Json::object({{"name", Json("echo")}, {"description", Json("d")},
{"parameters", Json::object({{"type", Json("object")}})}})}})};
}
std::shared_ptr<mcp::McpServeAgent> createAgent() override { return std::make_shared<FakeAgent>(); }
int maxSteps() override { return 25; }
std::string complete(const std::vector<Json>&) override { return "done"; }
};
class TestStore : public mcp::McpSqliteStore {
public:
using McpSqliteStore::McpSqliteStore;
std::optional<std::string> loadSecret(const std::string&, const std::string&) override { return std::nullopt; }
void saveSecret(const std::string&, const std::string&, const Json&, const std::string&) override {}
void deleteSecret(const std::string&, const std::string&) override {}
Json secretMeta(const std::string&, const std::string&) override { return Json::object(); }
std::string spillRecord(const std::string&, const std::string&, const std::string&) override { return "mem:1"; }
void auditEvent(const std::string&, const std::string&, const std::string&, const std::string&,
const std::string&) override {}
};
int main() {
// --- CAP tables
CHECK(cap::CAP_ERROR_CODES.size() == 15);
CHECK(cap::CapError("cap_budget_exceeded", "x").numeric == -32004);
CHECK(cap::CapError("cap_custom").numeric == 0);
CHECK(std::string(cap::CapError("cap_custom").what()) == "cap_custom");
CHECK(cap::CapEvent::KINDS().size() == 19);
for (const auto& kind : cap::CapEvent::KINDS()) CHECK(cap::CapEvent::DIRECTIONS(kind) != nullptr);
Json ev = cap::CapEvent::make("cap.text_chunk", Json::object({{"message_id", Json("m1")}, {"text", Json("hi")},
{"channel", Json("assistant")}}));
CHECK(ev.dumps() == "{\"kind\": \"cap.text_chunk\", \"message_id\": \"m1\", \"text\": \"hi\", \"channel\": \"assistant\"}");
bool threw = false;
try {
cap::CapEvent::make("cap.text_chunk", Json::object({{"message_id", Json("m1")}, {"text", Json("hi")}}));
} catch (const wire::ValueError&) {
threw = true;
}
CHECK(threw);
CHECK(cap::CapBinding::select(Json::object({{"fast_path", Json::object({{"grpc", Json(true)}})}})) == "pty");
cap::CapManifest manifest = cap::CapManifest::fromToml(cap::CapAgentManifest::TAI_CAP_MANIFEST(), "own");
CHECK(manifest.name() == "tai");
CHECK(manifest.binding() == "stream_json");
CHECK(manifest.get("parse.idle").items().size() == 1);
CHECK(cap::CapStreamJson::parseUserText("{\"type\":\"user\",\"message\":\"x\"}") == "x");
// --- drift check against ../cap.md
const std::string capDoc = slurp("../cap.md");
if (!capDoc.empty()) {
for (const auto& kind : cap::CapEvent::KINDS()) CHECK(capDoc.find("`" + kind + "`") != std::string::npos);
for (const auto& entry : cap::CAP_ERROR_CODES) CHECK(capDoc.find(std::string(entry.second)) != std::string::npos);
for (const auto& code : cap::CAP_ERROR_STRINGS) CHECK(capDoc.find("`" + std::string(code) + "`") != std::string::npos);
CHECK(capDoc.find("\xE2\x80\x94") == std::string::npos);
} else {
std::printf("note: ../cap.md not found, doc drift check skipped\n");
}
const std::string mcpDoc = slurp("../mcp.md");
if (!mcpDoc.empty()) {
for (const char* name : {"MCP_CONNECT_TIMEOUT", "MCP_CALL_TIMEOUT", "MCP_RESULT_MAX", "MCP_HTTP_MAX_BODY",
"MCP_MAX_SERVERS", "MCP_MAX_TOOLS", "MCP_SESSION_IDLE", "MCP_CHAT_TIMEOUT",
"MCP_RESOLVER_TIMEOUT", "MCP_RESOLVER_STEPS", "MCP_OAUTH_TIMEOUT"})
CHECK(mcpDoc.find(name) != std::string::npos);
}
// --- normalizer
CHECK(mcp::McpNormalizer::mcpSanitizeName("Foo Bar!") == "foo_bar");
CHECK(mcp::McpNormalizer::mcpSanitizeName("!!") == "server");
auto expanded = mcp::McpNormalizer::mcpExpandVars("a ${X} ${Y:-d} ${Z}", [](const std::string& n) -> std::optional<std::string> {
if (n == "X") return std::string("1");
return std::nullopt;
});
CHECK(expanded.first == "a 1 d ${Z}");
CHECK(expanded.second.size() == 1 && expanded.second[0] == "Z");
auto norm = mcp::McpNormalizer::mcpNormalize("{\"mcpServers\": {\"s\": {\"command\": \"npx\", \"args\": [\"-y\", \"p\"]}}}");
CHECK(norm.entry.get("transport").str() == "stdio");
CHECK(norm.entry.get("source").str() == "config-json");
threw = false;
try {
mcp::McpNormalizer::mcpNormalize("https://github.com/o/r");
} catch (const mcp::McpError&) {
threw = true;
}
CHECK(threw);
CHECK(mcp::McpNormalizer::mcpBridgeName("My Srv", "do-it") == "mcp_my_srv_do_it");
// --- vault tables
sqlite3* db = nullptr;
sqlite3_open(":memory:", &db);
mcp::McpPlainSeal seal;
TestStore store(db, &seal);
store.ensureSchema();
store.mcpSaveServer(Json::object({{"name", Json("srv")}, {"transport", Json("http")}, {"url", Json("https://x.test/mcp")},
{"headers", Json::object({{"Authorization", Json("Bearer s")}})}}));
CHECK(store.mcpGetServer("srv").get("url").str() == "https://x.test/mcp");
CHECK(!store.mcpServerEnabled("srv", "t"));
store.mcpSetEnabled("srv", "t", true);
CHECK(store.mcpEnabledServers("t").size() == 1);
store.mcpStoreTools("srv", {Json::object({{"name", Json("b")}, {"description", Json("bee")}}),
Json::object({{"name", Json("a")}})});
CHECK(store.mcpCachedTools("srv")[0].get("tool").str() == "a");
CHECK(store.mcpToolPins("srv").size() == 2);
threw = false;
try {
store.mcpSaveServer(Json::object({{"name", Json("bad name!")}}));
} catch (const wire::ValueError&) {
threw = true;
}
CHECK(threw);
store.mcpRemoveServer("srv");
CHECK(store.mcpGetServer("srv").isNull());
sqlite3_close(db);
// --- served endpoint, in process
FakeBackend backend;
mcp::McpServeContext ctx(backend, "k");
mcp::McpServeHandler handler(ctx);
wire::WireHttpServer::Request req;
wire::WireHttpServer::Response resp;
req.method = "POST";
req.path = "/mcp";
req.headers["authorization"] = "Bearer k";
req.body = "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{}}";
req.headers["content-length"] = std::to_string(req.body.size());
CHECK(handler.handle(req, resp) && resp.status == 200);
CHECK(Json::parse(resp.body).get("result").get("protocolVersion").str() == "2025-11-25");
std::string sid;
for (const auto& h : resp.headers)
if (h.first == "Mcp-Session-Id") sid = h.second;
CHECK(sid.size() == 32);
req.headers["mcp-session-id"] = sid;
req.body = "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"deny\",\"arguments\":{}}}";
req.headers["content-length"] = std::to_string(req.body.size());
handler.handle(req, resp);
CHECK(Json::parse(resp.body).get("result").get("isError").truthy());
req.headers["authorization"] = "Bearer wrong";
handler.handle(req, resp);
CHECK(resp.status == 401);
std::printf(failures ? "%d FAILURES\n" : "capmcp_unit: all checks passed\n", failures);
return failures ? 1 : 0;
}