feat: 添加 OpenNexus 认证 Core 与 Stronghold 基础能力
This commit is contained in:
@@ -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"
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user