feat: 添加 OpenNexus 认证 Core 与 Stronghold 基础能力

This commit is contained in:
2026-09-08 12:23:20 +08:00
parent f4aeeef49b
commit 4c79e940d2
59 changed files with 4242 additions and 102 deletions
+1 -1
View File
@@ -258,7 +258,7 @@ def test_mcp_lifecycle_lock_contention_keeps_event_loop_responsive(
def test_service_status() -> None:
response = asyncio.run(service_status())
assert response.name == "Notes Agent AI Core"
assert response.name == "OpenNexus AI Core"
assert response.status == "ok"
+13
View File
@@ -36,6 +36,19 @@ def test_encrypted_credential_store_round_trip_without_plaintext_on_disk() -> No
assert store.resolve("deepseek") is None
def test_migrated_fernet_owner_blocks_old_reads_and_writes() -> None:
store = EncryptedCredentialStore()
store.put("fixture", "test-secret")
directory = get_settings().data_dir / "credentials"
before = (directory / "credentials.json").read_bytes()
(directory / ".opennexus-owner.json").write_text('{"state":"switched"}')
for operation in [lambda: store.resolve("fixture"), lambda: store.has("fixture"),
lambda: store.put("fixture", "changed"), lambda: store.delete("fixture")]:
with pytest.raises(CredentialStoreError, match="CREDENTIAL_OWNER_DESKTOP"):
operation()
assert (directory / "credentials.json").read_bytes() == before
def test_encrypted_credential_store_deletes_multiple_credentials_atomically() -> None:
store = EncryptedCredentialStore()
store.put("plugin.first", "first")
+107
View File
@@ -0,0 +1,107 @@
import asyncio
import json
import os
from pathlib import Path
import queue
import subprocess
import sys
import threading
import urllib.error
import urllib.request
import pytest
from app.sidecar import SessionAuth, bootstrap, proof
def test_bootstrap_is_bounded_and_requires_session_entropy(tmp_path):
data = dict(protocol=1, secret="01" * 32, challenge="02" * 32,
generation="03" * 32, data_dir=str(tmp_path), launcher_pid=123)
assert bootstrap(json.dumps(data).encode() + b"\n") == data
for invalid in [b"{}\n", b"x" * 16385, b"{}", b"null\n"]:
with pytest.raises(ValueError):
bootstrap(invalid)
data["secret"] = "short"
with pytest.raises(ValueError):
bootstrap(json.dumps(data).encode() + b"\n")
def test_session_auth_covers_every_route_and_rejects_duplicate_headers():
calls = []
async def app(scope, receive, send):
calls.append(scope["path"])
await send({"type": "http.response.start", "status": 204, "headers": []})
auth = SessionAuth(app, "ab" * 32, "cd" * 32, 4567)
valid = [(b"host", b"127.0.0.1:4567"),
(b"authorization", ("Bearer " + "ab" * 32).encode()),
(b"x-core-generation", ("cd" * 32).encode())]
async def request(headers, path):
messages = []
async def send(message):
messages.append(message)
await auth({"type": "http", "headers": headers, "path": path}, None, send)
return messages[0]["status"]
for path in ["/health", "/api/status", "/api/events", "/api/export/file", "/docs", "/unknown"]:
for bad in [[], valid[:2], valid + [valid[1]],
valid + [(b"origin", b"tauri://localhost")],
[(b"host", b"evil.test")] + valid[1:],
valid[:2] + [(b"x-core-generation", b"old")]]:
assert asyncio.run(request(bad, path)) == 401
assert asyncio.run(request(valid, path)) == 204
assert len(calls) == 6
def test_handshake_proof_binds_port_pid_generation_and_challenge():
args = ["01" * 32, "02" * 32, "03" * 32, 123, 4567, 123]
expected = proof(*args)
assert len(expected) == 64
for i in range(1, len(args)):
changed = args.copy()
changed[i] = "04" * 32 if isinstance(args[i], str) else args[i] + 1
assert proof(*changed) != expected
def test_real_sidecar_bootstrap_auth_and_parent_eof(tmp_path):
config = dict(protocol=1, secret="01" * 32, challenge="02" * 32,
generation="03" * 32, data_dir=str(tmp_path / "core"))
executable = os.environ.get("OPENNEXUS_CORE_TEST_BINARY")
command = [executable] if executable else [sys.executable, "-m", "app.sidecar"]
diagnostics = (tmp_path / "core-stderr.log").open("wb")
process = subprocess.Popen(command,
cwd=Path(__file__).resolve().parents[1],
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=diagnostics,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
try:
config["launcher_pid"] = process.pid
process.stdin.write(json.dumps(config).encode() + b"\n")
process.stdin.flush()
received = queue.Queue()
threading.Thread(target=lambda: received.put(process.stdout.readline(16385)), daemon=True).start()
line = received.get(timeout=30)
assert line, (tmp_path / "core-stderr.log").read_text(encoding="utf-8", errors="replace")[-4000:]
ready = json.loads(line)
assert ready["launcher_pid"] == process.pid
assert ready["pid"] > 0
assert ready["proof"] == proof(config["secret"], config["challenge"], config["generation"],
ready["pid"], ready["port"], process.pid)
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
url = f'http://127.0.0.1:{ready["port"]}'
with pytest.raises(urllib.error.HTTPError) as error:
opener.open(url + "/health", timeout=5)
assert error.value.code == 401
request = urllib.request.Request(url + "/health", headers={
"Authorization": "Bearer " + config["secret"],
"X-Core-Generation": config["generation"],
})
with opener.open(request, timeout=5) as response:
assert json.load(response)["status"] == "ok"
process.stdin.close()
assert process.wait(timeout=10) == 0
finally:
if not process.stdin.closed:
process.stdin.close()
if process.poll() is None:
process.kill()
process.wait(timeout=10)
diagnostics.close()