feat: implement GEWU service and local agent workflows
CI / quality (push) Canceled after 0s

This commit is contained in:
vasily committed 2026-10-14 19:21:35 +08:00
1 parent 4db3f45164
commit d25c04cdd2
175 files changed
+56243 -15

No files matched your search

+1
View File
@@ -0,0 +1 @@
"""Transport boundary tests."""
+264
View File
@@ -0,0 +1,264 @@
import asyncio
import sqlite3
from pathlib import Path
import httpx2
from gewu.adapters.git_repository import GitCanonicalRepository
from gewu.adapters.sqlite_identity import SQLiteIdentityStore
from gewu.application.reading import CanonicalNodeReader
from gewu.bootstrap.app import create_app
PASSWORD = "initial-password-fixture"
def test_password_accounts_owned_agent_keys_and_session_lifecycle(tmp_path: Path) -> None:
async def exercise() -> None:
store = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
operator = store.provision(
credential_id="operator", principal_id="operator", roles=("admin",)
)
app = create_app(
identity_provider=store,
account_identity=store,
credential_administration=store,
node_reader=CanonicalNodeReader(GitCanonicalRepository(tmp_path / "knowledge.git")),
)
transport = httpx2.ASGITransport(app=app)
operator_headers = {"Authorization": f"Bearer {operator}"}
async with httpx2.AsyncClient(transport=transport, base_url="https://gewu.test") as admin:
body = {
"username": "alice",
"password": PASSWORD,
"role": "editor",
"project_ids": ["pilot"],
}
assert (await admin.post("/v1/admin/users", json=body)).status_code == 401
created = await admin.post("/v1/admin/users", json=body, headers=operator_headers)
assert created.status_code == 201 and PASSWORD not in created.text
assert (
await admin.post("/v1/admin/users", json=body, headers=operator_headers)
).status_code == 409
for username, role in (("bob", "editor"), ("viewer", "viewer"), ("admin", "admin")):
assert (
await admin.post(
"/v1/admin/users",
headers=operator_headers,
json={**body, "username": username, "role": role},
)
).status_code == 201
short_secret = "shortsecret"
invalid = await admin.post(
"/v1/admin/users",
headers=operator_headers,
json={**body, "username": "invalid", "password": short_secret},
)
assert invalid.status_code == 422 and short_secret not in invalid.text
listing = await admin.get("/v1/admin/users", headers=operator_headers)
assert "password_hash" not in listing.text and PASSWORD not in listing.text
async with httpx2.AsyncClient(
transport=transport, base_url="https://gewu.test"
) as alice:
login = await alice.post(
"/v1/users/session",
json={"username": "alice", "password": PASSWORD},
headers={"Origin": "https://gewu.test"},
)
assert login.status_code == 200
assert "secure" in login.headers["set-cookie"].lower()
csrf = {"X-GEWU-CSRF": login.json()["csrf_token"]}
assert (await alice.get("/v1/me")).json()["principal_id"] == "user:alice"
assert (await alice.get("/v1/admin/users")).status_code == 403
assert (
await alice.post("/v1/admin/users", headers=csrf, json=body)
).status_code == 403
agent_body = {"name": "codex-laptop", "project_ids": ["pilot"]}
assert (
await alice.post("/v1/me/agent-credentials", json=agent_body)
).status_code == 403
assert (
await alice.post(
"/v1/me/agent-credentials",
headers={**csrf, "Origin": "https://attacker"},
json=agent_body,
)
).status_code == 403
assert (
await alice.post(
"/v1/me/agent-credentials",
headers=csrf,
json={**agent_body, "project_ids": ["other"]},
)
).status_code == 403
assert (
await alice.post(
"/v1/me/agent-credentials",
headers=csrf,
json={**agent_body, "role": "admin"},
)
).status_code == 422
agent = await alice.post("/v1/me/agent-credentials", headers=csrf, json=agent_body)
assert agent.status_code == 201
key_id, token = agent.json()["credential_id"], agent.json()["token"]
agent_principal = store.authenticate(token)
assert agent_principal and agent_principal.can_submit_project("pilot")
assert not agent_principal.can_submit_project("other")
assert (await alice.get("/v1/me/agent-credentials")).json()[0][
"credential_id"
] == key_id
assert token not in (await alice.get("/v1/me/agent-credentials")).text
assert (
await alice.get("/v1/me", headers={"Authorization": f"Bearer {token}"})
).status_code == 403
async with httpx2.AsyncClient(
transport=transport, base_url="https://gewu.test"
) as bob:
bob_login = await bob.post(
"/v1/users/session", json={"username": "bob", "password": PASSWORD}
)
bob_csrf = {"X-GEWU-CSRF": bob_login.json()["csrf_token"]}
assert (await bob.get("/v1/me/agent-credentials")).json() == []
assert (
await bob.delete(f"/v1/me/agent-credentials/{key_id}", headers=bob_csrf)
).status_code == 404
assert store.authenticate(token) is not None
bob_agent = await bob.post(
"/v1/me/agent-credentials", headers=bob_csrf, json=agent_body
)
rotated = store.rotate(bob_agent.json()["credential_id"], "rotated-bob-key")
assert (await bob.get("/v1/me/agent-credentials")).json()[-1][
"owner_principal_id"
] == "user:bob"
assert (
await admin.delete(
"/v1/admin/credentials/account:bob", headers=operator_headers
)
).status_code == 200
assert store.authenticate(rotated) is None
assert (await bob.get("/v1/me")).status_code == 401
password_body = {
"current_password": PASSWORD,
"new_password": "changed-password-fixture",
}
assert (await alice.post("/v1/me/password", json=password_body)).status_code == 403
assert (
await alice.post(
"/v1/me/password",
headers=csrf,
json={**password_body, "current_password": "wrong"},
)
).status_code == 403
second_session = store.login_account("alice", PASSWORD)
assert second_session
changed = await alice.post("/v1/me/password", headers=csrf, json=password_body)
assert changed.status_code == 200
assert store.authenticate_browser_session(second_session.session_token) is None
assert (await alice.get("/v1/me")).status_code == 401
assert store.login_account("alice", PASSWORD) is None
assert store.authenticate(token) is not None
relogin = await alice.post(
"/v1/users/session",
json={"username": "alice", "password": password_body["new_password"]},
)
csrf = {"X-GEWU-CSRF": relogin.json()["csrf_token"]}
assert (
await alice.delete(f"/v1/me/agent-credentials/{key_id}", headers=csrf)
).status_code == 200
assert store.authenticate(token) is None
another = await alice.post(
"/v1/me/agent-credentials", headers=csrf, json=agent_body
)
assert (
await admin.delete("/v1/admin/users/alice", headers=operator_headers)
).status_code == 200
assert store.authenticate(another.json()["token"]) is None
assert (await alice.get("/v1/me")).status_code == 401
assert store.login_account("alice", password_body["new_password"]) is None
async with httpx2.AsyncClient(
transport=transport, base_url="https://gewu.test"
) as viewer:
login = await viewer.post(
"/v1/users/session", json={"username": "viewer", "password": PASSWORD}
)
csrf = {"X-GEWU-CSRF": login.json()["csrf_token"]}
assert (
await viewer.post(
"/v1/me/agent-credentials",
headers=csrf,
json={"name": "agent", "project_ids": ["pilot"]},
)
).status_code == 403
async with httpx2.AsyncClient(
transport=transport, base_url="https://gewu.test"
) as human_admin:
login = await human_admin.post(
"/v1/users/session", json={"username": "admin", "password": PASSWORD}
)
csrf = {"X-GEWU-CSRF": login.json()["csrf_token"]}
assert (
await human_admin.delete("/v1/admin/users/admin", headers=csrf)
).status_code == 403
with sqlite3.connect(store.database) as db:
assert (
db.execute("SELECT password_hash FROM user_accounts WHERE username='bob'")
.fetchone()[0]
.startswith("$argon2id$")
)
assert (
PASSWORD not in (await admin.get("/v1/admin/events", headers=operator_headers)).text
)
assert len(SQLiteIdentityStore(store.database).list_accounts(limit=50, offset=0)) == 4
asyncio.run(exercise())
def test_password_login_origin_tls_size_and_throttle(tmp_path: Path) -> None:
async def exercise() -> None:
store = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
store.create_account(
username="alice",
password=PASSWORD,
role="editor",
project_ids=("pilot",),
actor="operator",
)
app = create_app(
identity_provider=store,
account_identity=store,
node_reader=CanonicalNodeReader(GitCanonicalRepository(tmp_path / "knowledge.git")),
)
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app), base_url="http://gewu.test"
) as http:
body = {"username": "alice", "password": PASSWORD}
assert (await http.post("/v1/users/session", json=body)).status_code == 403
assert (
await http.post(
"https://gewu.test/v1/users/session",
json=body,
headers={"Origin": "https://attacker.invalid"},
)
).status_code == 403
assert (
await http.post("https://gewu.test/v1/users/session", content=b" " * 4097)
).status_code == 413
for _ in range(5):
assert (
await http.post(
"https://gewu.test/v1/users/session", json={**body, "password": "wrong"}
)
).status_code == 401
assert (
await http.post("https://gewu.test/v1/users/session", json=body)
).status_code == 401
with store._connect() as db:
db.execute("UPDATE account_login_failures SET failed_at=0")
assert (
await http.post("https://gewu.test/v1/users/session", json=body)
).status_code == 200
asyncio.run(exercise())
+31
View File
@@ -0,0 +1,31 @@
import json
from pathlib import Path
from scripts.export_agent_contract import PACK, build_contract
def test_pack_mcp_contract_matches_the_live_server_and_generated_schemas(
tmp_path: Path,
) -> None:
snapshot_path = PACK / "contracts" / "service-v1.json"
snapshot = json.loads(snapshot_path.read_text(encoding="utf-8"))
actual = build_contract(tmp_path)
assert actual == snapshot
assert (PACK / "contracts" / "catalog-v1.json").read_bytes() == (
Path(__file__).resolve().parents[2] / "schema" / "v1" / "catalog.json"
).read_bytes()
packaged_schemas = PACK / "contracts"
for schema in snapshot["record_schemas"]:
schema_path = packaged_schemas / schema["path"]
assert schema_path.is_file()
assert (
schema_path.read_bytes()
== (
Path(__file__).resolve().parents[2]
/ "schema"
/ "v1"
/ "records"
/ Path(schema["path"]).name
).read_bytes()
)
+273
View File
@@ -0,0 +1,273 @@
import asyncio
import hashlib
from pathlib import Path
import httpx
import yaml
from gewu.adapters.git_repository import GitCanonicalRepository
from gewu.adapters.sqlite_identity import SQLiteIdentityStore
from gewu.adapters.text_extractor import TextMarkdownExtractor
from gewu.application.reading import CanonicalNodeReader
from gewu.application.uploads import LocalArtifactStore
from gewu.bootstrap.app import create_app
from tests.domain.proposal_fixtures import NODE, SOURCE_REVISION
def test_authenticated_scoped_canonical_node_read(tmp_path: Path) -> None:
async def exercise() -> None:
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
project_key = hashlib.sha256(NODE.project_id.encode()).hexdigest()
node_key = hashlib.sha256(NODE.node_id.encode()).hexdigest()
node_path = f"projects/{project_key}/nodes/{node_key}.yaml"
source_path = (
f"sources/{hashlib.sha256(SOURCE_REVISION.source_id.encode()).hexdigest()}/"
f"{hashlib.sha256(SOURCE_REVISION.revision_id.encode()).hexdigest()}.yaml"
)
artifact_store = LocalArtifactStore(tmp_path / "artifacts")
source_bytes = b"# Archived source\nA precise citation.\n"
source_digest = hashlib.sha256(source_bytes).hexdigest()
upload_id = artifact_store.begin(
filename="source.md",
media_type="text/markdown",
byte_length=len(source_bytes),
expected_sha256=source_digest,
owner_id="agent-pilot",
project_id=NODE.project_id,
).upload_id
artifact_store.put_chunk(
upload_id,
offset=0,
content=source_bytes,
chunk_sha256=source_digest,
owner_id="agent-pilot",
)
artifact_store.finalize(upload_id, owner_id="agent-pilot")
extraction = TextMarkdownExtractor().extract(
SOURCE_REVISION.revision_id, source_bytes, "text/markdown"
)
artifact_store.save_extraction(
extraction, owner_id="agent-pilot", project_id=NODE.project_id
)
archived_source = SOURCE_REVISION.model_copy(
update={"artifact_sha256": source_digest, "byte_length": len(source_bytes)}
)
repository.publish_files(
expected_head=repository.head(),
files={node_path: yaml.safe_dump(NODE.model_dump(mode="json")).encode()},
message="Publish test node",
index_directory=tmp_path / "publishing",
)
updated_node = NODE.model_copy(update={"summary": "A revised accepted summary."})
restricted_source = SOURCE_REVISION.model_copy(
update={
"source_id": "video:private",
"revision_id": "video:private:r1",
"ingested_for": ("interactive-video",),
}
)
restricted_source_path = (
f"sources/{hashlib.sha256(restricted_source.source_id.encode()).hexdigest()}/"
f"{hashlib.sha256(restricted_source.revision_id.encode()).hexdigest()}.yaml"
)
reference_source = SOURCE_REVISION.model_copy(
update={
"source_id": "dome:reference-only",
"revision_id": "dome:reference-only:r1",
"artifact_sha256": None,
"media_type": "application/octet-stream",
"byte_length": 0,
"original_url": "https://research.example/unavailable-paper",
"ingested_for": (NODE.project_id,),
"archive_status": "reference-only",
}
)
reference_source_path = (
f"sources/{hashlib.sha256(reference_source.source_id.encode()).hexdigest()}/"
f"{hashlib.sha256(reference_source.revision_id.encode()).hexdigest()}.yaml"
)
repository.publish_files(
expected_head=repository.head(),
files={
node_path: yaml.safe_dump(updated_node.model_dump(mode="json")).encode(),
source_path: yaml.safe_dump(archived_source.model_dump(mode="json")).encode(),
restricted_source_path: yaml.safe_dump(
restricted_source.model_dump(mode="json")
).encode(),
reference_source_path: yaml.safe_dump(
reference_source.model_dump(mode="json")
).encode(),
},
message="Revise node and register source",
index_directory=tmp_path / "publishing",
)
repository.publish_files(
expected_head=repository.head(),
files={"metadata/unrelated.yaml": b"accepted but unrelated\n"},
message="Advance repository without changing the node",
index_directory=tmp_path / "publishing",
)
identities = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
token = identities.provision(
credential_id="agent-key",
principal_id="agent-pilot",
roles=("agent",),
project_ids=(NODE.project_id,),
)
app = create_app(
identity_provider=identities,
node_reader=CanonicalNodeReader(repository),
artifact_store=artifact_store,
)
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=app), base_url="http://gewu.test"
) as client:
path = f"/v1/projects/{NODE.project_id}/nodes/{NODE.node_id}"
assert (await client.get(path)).status_code == 401
malformed_id = await client.get(
f"/v1/projects/{NODE.project_id}/nodes/not%20a%20record-id",
headers={"Authorization": f"Bearer {token}"},
)
assert malformed_id.status_code == 422
denied = await client.get(
f"/v1/projects/interactive-video/nodes/{NODE.node_id}",
headers={"Authorization": f"Bearer {token}"},
)
assert denied.status_code == 403
accepted = await client.get(path, headers={"Authorization": f"Bearer {token}"})
assert accepted.status_code == 200
envelope = accepted.json()
assert envelope["retrieval_mode"] == "canonical"
assert envelope["canonical_revision"] == repository.head()
assert envelope["data"][0]["node_id"] == NODE.node_id
assert envelope["citations"][0]["quote"] == NODE.claims[0].evidence[0].quote
history = await client.get(
f"{path}/history", headers={"Authorization": f"Bearer {token}"}
)
assert history.status_code == 200
history_data = history.json()["data"]
assert len(history_data) == 2
assert history_data[0]["node"]["summary"] == updated_node.summary
latest_node_revision = history_data[0]["revision"]
assert latest_node_revision != repository.head()
latest_file_version = await client.get(
f"{path}?revision={latest_node_revision}",
headers={"Authorization": f"Bearer {token}"},
)
assert latest_file_version.status_code == 200
assert latest_file_version.json()["canonical_revision"] == latest_node_revision
assert latest_file_version.json()["data"][0]["summary"] == updated_node.summary
historical_revision = history_data[-1]["revision"]
historical = await client.get(
f"{path}?revision={historical_revision}",
headers={"Authorization": f"Bearer {token}"},
)
assert historical.status_code == 200, historical.text
assert historical.json()["canonical_revision"] == historical_revision
assert historical.json()["data"][0]["summary"] == NODE.summary
assert historical.json()["citations"][0]["quote"] == NODE.claims[0].evidence[0].quote
malformed_revision = await client.get(
f"{path}?revision=not-a-commit",
headers={"Authorization": f"Bearer {token}"},
)
assert malformed_revision.status_code == 422
unknown_revision = await client.get(
f"{path}?revision={'a' * 40}",
headers={"Authorization": f"Bearer {token}"},
)
assert unknown_revision.status_code == 404
source = await client.get(
f"/v1/projects/{NODE.project_id}/sources/{SOURCE_REVISION.revision_id}",
headers={"Authorization": f"Bearer {token}"},
)
assert source.status_code == 200
assert source.json()["data"][0]["revision_id"] == SOURCE_REVISION.revision_id
reference_path = (
f"/v1/projects/{NODE.project_id}/sources/{reference_source.revision_id}"
)
reference = await client.get(
reference_path, headers={"Authorization": f"Bearer {token}"}
)
assert reference.status_code == 200
assert reference.json()["data"][0]["artifact_sha256"] is None
diagnostics_path = "/v1/diagnostics?projects=dome-performance"
assert (await client.get("/v1/diagnostics")).status_code == 401
diagnostic_response = await client.get(
diagnostics_path, headers={"Authorization": f"Bearer {token}"}
)
assert diagnostic_response.status_code == 200
diagnostic_body = diagnostic_response.json()
catalog_path = Path(__file__).resolve().parents[2] / "schema/v1/catalog.json"
assert (
diagnostic_body["catalog_sha256"]
== hashlib.sha256(catalog_path.read_bytes()).hexdigest()
)
assert diagnostic_body["selected_scope"]["projects"] == [NODE.project_id]
assert diagnostic_body["index_state"] == "not_configured"
assert diagnostic_body["max_upload_bytes"] == 50 * 1024 * 1024
assert "get_source_artifact" in diagnostic_body["authorized_operations"]
assert token not in diagnostic_response.text
assert "principal_id" not in diagnostic_body
forbidden_diagnostic_scope = await client.get(
"/v1/diagnostics?projects=interactive-video",
headers={"Authorization": f"Bearer {token}"},
)
assert forbidden_diagnostic_scope.status_code == 403
forbidden_shared_scope = await client.get(
diagnostics_path + "&include_shared=true",
headers={"Authorization": f"Bearer {token}"},
)
assert forbidden_shared_scope.status_code == 403
artifact_path = (
f"/v1/projects/{NODE.project_id}/sources/{SOURCE_REVISION.revision_id}/artifact"
)
assert (await client.get(artifact_path)).status_code == 401
archived = await client.get(artifact_path, headers={"Authorization": f"Bearer {token}"})
assert archived.status_code == 200
assert archived.content == source_bytes
assert archived.headers["etag"] == f'"{source_digest}"'
assert archived.headers["x-content-type-options"] == "nosniff"
assert archived.headers["cache-control"] == "private, no-store"
assert archived.headers["content-security-policy"] == "default-src 'none'; sandbox"
missing_artifact = await client.get(
f"{reference_path}/artifact", headers={"Authorization": f"Bearer {token}"}
)
assert missing_artifact.status_code == 404
block = extraction.blocks[0]
offset = block.normalized_text.index("precise")
location_path = (
f"/v1/projects/{NODE.project_id}/sources/{SOURCE_REVISION.revision_id}"
f"/extractions/{extraction.extraction_id}/blocks/{block.block_id}"
)
location = await client.get(
location_path,
params={"offset": offset, "length": len("precise")},
headers={"Authorization": f"Bearer {token}"},
)
assert location.status_code == 200
assert location.json()["normalized_text"] == "precise"
assert location.json()["original_text"] == "precise"
denied_location = await client.get(
location_path,
params={"offset": offset, "length": len("precise")},
)
assert denied_location.status_code == 401
restricted = await client.get(
f"/v1/projects/{NODE.project_id}/sources/{restricted_source.revision_id}",
headers={"Authorization": f"Bearer {token}"},
)
assert restricted.status_code == 404
restricted_artifact = await client.get(
f"/v1/projects/{NODE.project_id}/sources/{restricted_source.revision_id}/artifact",
headers={"Authorization": f"Bearer {token}"},
)
assert restricted_artifact.status_code == 404
assert identities.revoke("agent-key")
assert (
await client.get(path, headers={"Authorization": f"Bearer {token}"})
).status_code == 401
assert (
await client.get(artifact_path, headers={"Authorization": f"Bearer {token}"})
).status_code == 401
asyncio.run(exercise())
+189
View File
@@ -0,0 +1,189 @@
import asyncio
from pathlib import Path
import httpx2
from gewu.adapters.git_repository import GitCanonicalRepository
from gewu.adapters.sqlite_identity import SQLiteIdentityStore
from gewu.adapters.sqlite_search import SQLiteSearchIndex
from gewu.application.reading import CanonicalNodeReader
from gewu.application.search import CanonicalSearchService
from gewu.bootstrap.app import create_app
from tests.domain.proposal_fixtures import PROJECTS
def test_browser_session_is_http_only_human_only_and_csrf_protected(tmp_path: Path) -> None:
async def exercise() -> None:
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
identities = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
viewer_key = identities.provision(
credential_id="browser-viewer-key",
principal_id="browser-viewer",
roles=("viewer",),
project_ids=("dome-performance",),
)
agent_key = identities.provision(
credential_id="browser-agent-key",
principal_id="browser-agent",
roles=("agent",),
project_ids=("dome-performance",),
)
search = CanonicalSearchService(
index=SQLiteSearchIndex(tmp_path / "search.sqlite3", repository),
project_ids_provider=lambda: PROJECTS.keys(),
canonical_head=repository.head,
)
app = create_app(
identity_provider=identities,
node_reader=CanonicalNodeReader(repository),
search_service=search,
)
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app),
base_url="http://gewu.test",
) as client:
agent_session = await client.post(
"/v1/session",
headers={
"Authorization": f"Bearer {agent_key}",
"Origin": "http://gewu.test",
},
)
assert agent_session.status_code == 403
cross_origin = await client.post(
"/v1/session",
headers={
"Authorization": f"Bearer {viewer_key}",
"Origin": "https://attacker.invalid",
},
)
assert cross_origin.status_code == 403
created = await client.post(
"/v1/session",
headers={
"Authorization": f"Bearer {viewer_key}",
"Origin": "http://gewu.test",
},
)
assert created.status_code == 200, created.text
session = created.json()
cookie = created.cookies.get("gewu_session")
assert cookie is not None
assert "httponly" in created.headers["set-cookie"].lower()
assert "samesite=strict" in created.headers["set-cookie"].lower()
assert "max-age" not in created.headers["set-cookie"].lower()
projects = await client.get("/v1/projects")
assert projects.status_code == 200
assert projects.json() == ["dome-performance"]
denied_csrf = await client.post(
"/v1/search",
json={
"query": "visitors",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"budget_tokens": 5000,
},
)
assert denied_csrf.status_code == 403
searched = await client.post(
"/v1/search",
headers={"X-GEWU-CSRF": session["csrf_token"]},
json={
"query": "visitors",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"budget_tokens": 5000,
},
)
assert searched.status_code == 200, searched.text
signed_out = await client.delete(
"/v1/session", headers={"X-GEWU-CSRF": session["csrf_token"]}
)
assert signed_out.status_code == 200
assert (await client.get("/v1/projects")).status_code == 401
assert identities.authenticate_browser_session(cookie) is None
asyncio.run(exercise())
def test_session_restoration_preserves_other_tabs_and_original_expiry(tmp_path: Path) -> None:
async def exercise() -> None:
identities = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
token = identities.provision(
credential_id="admin", principal_id="operator", roles=("admin",)
)
app = create_app(
identity_provider=identities,
node_reader=CanonicalNodeReader(GitCanonicalRepository(tmp_path / "knowledge.git")),
)
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app), base_url="https://gewu.test"
) as http:
headers = {"X-GEWU-Session": "restore", "Origin": "https://gewu.test"}
assert (await http.get("/v1/session", headers=headers)).status_code == 401
login = await http.post("/v1/session", headers={"Authorization": f"Bearer {token}"})
original_csrf = login.json()["csrf_token"]
cookie = login.cookies.get("gewu_session")
assert cookie
assert (await http.get("/v1/session")).status_code == 403
assert (
await http.get(
"/v1/session", headers={**headers, "Origin": "https://attacker.invalid"}
)
).status_code == 403
assert (
await http.get("/v1/session", headers={**headers, "Sec-Fetch-Site": "cross-site"})
).status_code == 403
restored = await http.get("/v1/session", headers=headers)
assert restored.status_code == 200
assert restored.json()["expires_at"] == login.json()["expires_at"]
assert restored.json()["password_account"] is False
assert restored.headers["cache-control"] == "private, no-store"
assert "set-cookie" not in restored.headers
assert cookie not in restored.text and token not in restored.text
restored_csrf = restored.json()["csrf_token"]
assert identities.browser_csrf_valid(cookie, original_csrf)
assert identities.browser_csrf_valid(cookie, restored_csrf)
second = await http.get("/v1/session", headers=headers)
assert second.json()["csrf_token"] == restored_csrf
assert identities.browser_csrf_valid(cookie, original_csrf)
assert not identities.browser_csrf_valid(cookie, "gwc_invalid")
signed_out = await http.delete("/v1/session", headers={"X-GEWU-CSRF": restored_csrf})
assert signed_out.status_code == 200
http.cookies.set("gewu_session", cookie)
assert (await http.get("/v1/session", headers=headers)).status_code == 401
next_login = identities.create_browser_session(token)
http.cookies.clear()
http.cookies.set("gewu_session", next_login.session_token)
identities.revoke("admin")
assert (await http.get("/v1/session", headers=headers)).status_code == 401
asyncio.run(exercise())
def test_password_session_restore_and_pre_upgrade_session_migration(tmp_path: Path) -> None:
identities = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
identities.create_account(
username="admin",
password="fixture-password",
role="admin",
project_ids=(),
actor="operator",
)
session = identities.login_account("admin", "fixture-password")
assert session
# Reconstruct the pre-upgrade schema with an issued session, then run the migration.
with identities._connect() as db:
db.execute("ALTER TABLE browser_sessions DROP COLUMN restored_csrf_digest")
reopened = SQLiteIdentityStore(identities.database)
restored = reopened.restore_browser_session(session.session_token)
assert restored and restored.expires_at == session.expires_at
assert reopened.browser_csrf_valid(session.session_token, session.csrf_token)
assert reopened.browser_csrf_valid(session.session_token, restored.csrf_token)
with reopened._connect() as db:
db.execute("UPDATE browser_sessions SET expires_at=0")
assert reopened.restore_browser_session(session.session_token) is None
+103
View File
@@ -0,0 +1,103 @@
import asyncio
from pathlib import Path
import httpx2
from gewu.adapters.git_repository import GitCanonicalRepository
from gewu.adapters.sqlite_identity import SQLiteIdentityStore
from gewu.application.reading import CanonicalNodeReader
from gewu.bootstrap.app import create_app
def test_admin_credentials_sessions_and_secret_free_audit(tmp_path: Path) -> None:
async def exercise() -> None:
store = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
admin = store.provision(credential_id="admin", principal_id="operator", roles=("admin",))
viewer = store.provision(credential_id="viewer", principal_id="reader", roles=("viewer",))
mixed = store.provision(
credential_id="mixed", principal_id="agent", roles=("agent", "admin")
)
app = create_app(
identity_provider=store,
credential_administration=store,
node_reader=CanonicalNodeReader(GitCanonicalRepository(tmp_path / "knowledge.git")),
)
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app), base_url="http://test"
) as http:
assert (await http.get("/v1/admin/credentials")).status_code == 401
for token in (viewer, mixed):
headers = {"Authorization": f"Bearer {token}"}
assert (await http.get("/v1/admin/credentials", headers=headers)).status_code == 403
assert (await http.get("/v1/admin/events", headers=headers)).status_code == 403
assert (
await http.post(
"/v1/admin/credentials",
headers=headers,
json={
"credential_id": "denied",
"principal_id": "denied",
"role": "admin",
},
)
).status_code == 403
assert (
await http.delete("/v1/admin/credentials/viewer", headers=headers)
).status_code == 403
session = await http.post("/v1/session", headers={"Authorization": f"Bearer {admin}"})
csrf = {"X-GEWU-CSRF": session.json()["csrf_token"]}
body = {
"credential_id": "new",
"principal_id": "researcher",
"role": "agent",
"project_ids": ["pilot"],
}
assert (await http.post("/v1/admin/credentials", json=body)).status_code == 403
assert (
await http.post(
"/v1/admin/credentials", json=body, headers={**csrf, "Origin": "https://other"}
)
).status_code == 403
assert (
await http.post("/v1/admin/credentials", headers=csrf, content=b" " * 4097)
).status_code == 413
assert (
await http.post(
"/v1/admin/credentials", headers=csrf, json={**body, "project_ids": []}
)
).status_code == 422
created = await http.post("/v1/admin/credentials", headers=csrf, json=body)
assert created.status_code == 201
token = created.json()["token"]
principal = store.authenticate(token)
assert principal and principal.project_ids == frozenset({"pilot"})
assert (
await http.post("/v1/admin/credentials", headers=csrf, json=body)
).status_code == 409
listing = await http.get("/v1/admin/credentials")
assert listing.headers["cache-control"] == "private, no-store"
assert token not in listing.text and "token_digest" not in listing.text
assert (
await http.delete("/v1/admin/credentials/admin", headers=csrf)
).status_code == 409
assert (await http.delete("/v1/admin/credentials/new")).status_code == 403
assert (await http.delete("/v1/admin/credentials/new", headers=csrf)).status_code == 200
assert store.authenticate(token) is None
assert (await http.delete("/v1/admin/credentials/new", headers=csrf)).status_code == 404
# Revocation invalidates already-issued browser sessions as well as bearer keys.
viewer_session = store.create_browser_session(viewer)
assert (
await http.delete("/v1/admin/credentials/viewer", headers=csrf)
).status_code == 200
assert store.authenticate_browser_session(viewer_session.session_token) is None
events = await http.get("/v1/admin/events")
assert [e["action"] for e in events.json()] == [
"credential.revoked",
"credential.revoked",
"credential.created",
]
assert token not in events.text and admin not in events.text
reopened = SQLiteIdentityStore(store.database)
assert len(reopened.list_admin_events(limit=50)) == 3
asyncio.run(exercise())
+82
View File
@@ -0,0 +1,82 @@
import asyncio
from pathlib import Path
import httpx
import pytest
from _pytest.monkeypatch import MonkeyPatch
from fastapi.routing import APIRoute
from gewu.bootstrap.app import create_app, create_local_app
def test_health_is_available_without_exposing_mutation_routes() -> None:
async def request_health() -> httpx.Response:
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=create_app()),
base_url="http://gewu.test",
) as client:
return await client.get("/health")
response = asyncio.run(request_health())
assert response.status_code == 200
assert response.json() == {"status": "ok", "service": "gewu"}
routes = {
f"{method} {route.path}"
for route in create_app().routes
if isinstance(route, APIRoute)
for method in route.methods or ()
}
assert routes == {"GET /health"}
def test_local_factory_composes_private_persistent_read_service(
monkeypatch: MonkeyPatch, tmp_path: Path
) -> None:
data_root = tmp_path / "private-gewu"
monkeypatch.setenv("GEWU_DATA_ROOT", str(data_root))
app = create_local_app()
route_paths = {route.path for route in app.routes if isinstance(route, APIRoute)}
assert route_paths == {
"/health",
"/v1/diagnostics",
"/v1/admin/projects",
"/v1/admin/credentials",
"/v1/admin/credentials/{credential_id}",
"/v1/admin/events",
"/v1/admin/users",
"/v1/admin/users/{username}",
"/v1/users/session",
"/v1/me",
"/v1/me/password",
"/v1/me/agent-credentials",
"/v1/me/agent-credentials/{credential_id}",
"/v1/admin/projects/{project_id}/archive",
"/v1/projects/{project_id}/nodes/{node_id}",
"/v1/projects/{project_id}/nodes/{node_id}/history",
"/v1/projects/{project_id}/sources/{revision_id}",
"/v1/projects/{project_id}/sources/{revision_id}/artifact",
"/v1/projects/{project_id}/sources/{revision_id}/extractions/{extraction_id}/blocks/{block_id}",
"/v1/neighbors",
"/v1/projects",
"/v1/search",
"/v1/sources",
"/v1/session",
"/auth/owner-session",
}
assert (data_root / "knowledge.git" / "HEAD").is_file()
assert (data_root / "identity.sqlite3").is_file()
assert (data_root / "search.sqlite3").is_file()
assert (data_root / "search.sqlite3").stat().st_mode & 0o077 == 0
assert (data_root.stat().st_mode & 0o777) == 0o700
def test_local_factory_rejects_symlinked_data_root(
monkeypatch: MonkeyPatch, tmp_path: Path
) -> None:
target = tmp_path / "real-root"
target.mkdir()
link = tmp_path / "linked-root"
link.symlink_to(target, target_is_directory=True)
monkeypatch.setenv("GEWU_DATA_ROOT", str(link))
with pytest.raises(ValueError, match="symbolic links"):
create_local_app()
+951
View File
@@ -0,0 +1,951 @@
import asyncio
import base64
import hashlib
import json
import socket
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Any
import httpx2
from mcp import Client
from mcp.client.streamable_http import streamable_http_client
from mcp.types import TextContent
from gewu.adapters.git_repository import GitCanonicalRepository
from gewu.adapters.source_extractor import LocalSourceExtractor
from gewu.adapters.sqlite_identity import SQLiteIdentityStore
from gewu.adapters.url_fetch import PublicUrlFetcher
from gewu.application.acceptance import AcceptanceService
from gewu.application.context import ValidationContext
from gewu.application.reading import CanonicalNodeReader
from gewu.application.uploads import LocalArtifactStore
from gewu.bootstrap.app import create_app
from gewu.domain.contracts import ProposalOperation, ProposalRevision, Scope, SourceRevisionRecord
from gewu.domain.validation import evidence_digest, proposal_payload_digest
from gewu.transports.stdio_connector import SelectedFile, create_connector_server
from tests.domain.proposal_fixtures import (
BLOCK_TEXT,
NODE,
POLICIES,
PROJECTS,
SOURCE_REVISION,
proposal,
)
class _FixtureResponse:
def __init__(self, media_type: str, content: bytes, status: int = 200) -> None:
self.media_type = media_type
self.content = content
self.status = status
self.offset = 0
def getheader(self, name: str) -> str | None:
return {
"Content-Type": f"{self.media_type}; charset=utf-8",
"X-Source-Title": "Allowlisted fixture",
}.get(name)
def read(self, amount: int) -> bytes:
result = self.content[self.offset : self.offset + amount]
self.offset += len(result)
return result
class _FixtureConnection:
def __init__(self) -> None:
self.closed = False
self.path = ""
def request(self, _method: str, path: str, headers: dict[str, str]) -> None:
self.path = path
assert headers["Accept-Encoding"] == "identity"
return None
def getresponse(self) -> _FixtureResponse:
if self.path.startswith("/missing"):
return _FixtureResponse("text/plain", b"unavailable", status=404)
if self.path.startswith("/source.md?rev=2"):
return _FixtureResponse("text/markdown", b"# Captured fixture\nRevised evidence.\n")
if self.path.startswith("/page.html"):
return _FixtureResponse(
"text/html",
b"<article><h1>Web evidence</h1><p>Exact HTML statement.</p>"
b"<script>ignoreInjection()</script></article>",
)
if self.path.startswith("/paper.pdf"):
return _FixtureResponse("application/pdf", _pdf_fixture())
if self.path.startswith("/malformed.pdf"):
return _FixtureResponse("application/pdf", b"%PDF-1.4\nnot a valid PDF")
return _FixtureResponse(
"text/markdown", b"# Captured fixture\nAn exact source statement.\n"
)
def close(self) -> None:
self.closed = True
def _fixture_fetcher() -> PublicUrlFetcher:
def resolver(host: str, port: int, **_kwargs: Any) -> list[tuple[Any, ...]]:
return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("93.184.216.34", port))]
return PublicUrlFetcher(
{"research.example"},
resolver=resolver,
connection_factory=lambda *_args: _FixtureConnection(),
)
def _text_payload(result: Any) -> str:
assert result.content and isinstance(result.content[0], TextContent)
return result.content[0].text
def _pdf_fixture() -> bytes:
stream = b"BT /F1 12 Tf 72 720 Td (Archived PDF evidence.) Tj ET"
objects = [
b"<< /Type /Catalog /Pages 2 0 R >>",
b"<< /Type /Pages /Kids [4 0 R] /Count 1 >>",
b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] "
b"/Resources << /Font << /F1 3 0 R >> >> /Contents 5 0 R >>",
b"<< /Length " + str(len(stream)).encode() + b" >>\nstream\n" + stream + b"\nendstream",
]
result = bytearray(b"%PDF-1.4\n")
offsets = [0]
for number, item in enumerate(objects, start=1):
offsets.append(len(result))
result.extend(f"{number} 0 obj\n".encode() + item + b"\nendobj\n")
xref = len(result)
result.extend(f"xref\n0 {len(offsets)}\n0000000000 65535 f \n".encode())
for offset in offsets[1:]:
result.extend(f"{offset:010d} 00000 n \n".encode())
result.extend(
f"trailer\n<< /Size {len(offsets)} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n".encode()
)
return bytes(result)
def test_mcp_source_to_citation_vertical_slice(tmp_path: Path) -> None:
async def exercise() -> None:
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
artifacts = LocalArtifactStore(tmp_path)
extractor = LocalSourceExtractor()
context = ValidationContext(
projects=PROJECTS,
policies=POLICIES,
sources={},
extractions={},
existing_nodes={},
)
identities = SQLiteIdentityStore(
tmp_path / "identity.sqlite3",
authority_lock_path=tmp_path / "workflow/locks/canonical-writer.lock",
)
token = identities.provision(
credential_id="pilot-agent-key",
principal_id="agent-pilot",
roles=("agent",),
project_ids=("dome-performance", "shared"),
)
viewer_token = identities.provision(
credential_id="pilot-viewer-key",
principal_id="pilot-viewer",
roles=("viewer",),
project_ids=("dome-performance", "shared"),
)
acceptance = AcceptanceService(
repository=repository,
artifact_store=artifacts,
workflow_root=tmp_path / "workflow",
context_provider=lambda: context,
actor_is_current=lambda actor: identities.is_current(
actor.principal_id,
actor.credential_id,
roles=actor.roles,
project_ids=actor.project_ids,
),
)
app = create_app(
identity_provider=identities,
node_reader=CanonicalNodeReader(repository),
artifact_store=artifacts,
extractor=extractor,
acceptance_service=acceptance,
context_provider=lambda: context,
url_fetcher=_fixture_fetcher(),
)
async with app.router.lifespan_context(app):
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app),
base_url="http://localhost",
headers={"Authorization": f"Bearer {token}"},
) as http:
async with Client(
streamable_http_client("http://localhost/mcp", http_client=http)
) as mcp:
tools = await mcp.list_tools()
assert {tool.name for tool in tools.tools} == {
"diagnostics",
"get_node",
"node_history",
"get_project_context",
"get_source",
"source_upload_begin",
"source_capture_text",
"source_capture_url",
"source_upload_status",
"source_upload_chunk",
"source_upload_finalize",
"source_extract",
"source_extraction_block",
"proposal_evaluate",
"proposal_submit",
"proposal_status",
"proposal_withdraw",
"run_create",
"run_checkpoint",
"run_status",
}
project_context = await mcp.call_tool(
"get_project_context", {"project_id": "dome-performance"}
)
assert not project_context.is_error
lens_result = json.loads(_text_payload(project_context))
assert (
lens_result["lens"]["north_star"] == PROJECTS["dome-performance"].north_star
)
assert lens_result["lens"]["key_questions"] == list(
PROJECTS["dome-performance"].key_questions
)
assert "autonomy_policy_id" not in lens_result["lens"]
denied_lens = await mcp.call_tool(
"get_project_context", {"project_id": "interactive-video"}
)
assert denied_lens.is_error
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app),
base_url="http://localhost",
headers={"Authorization": f"Bearer {viewer_token}"},
) as viewer_http:
async with Client(
streamable_http_client("http://localhost/mcp", http_client=viewer_http)
) as viewer_mcp:
viewer_proposal = await viewer_mcp.call_tool(
"proposal_submit",
{"proposal_json": proposal().model_dump_json()},
)
assert viewer_proposal.is_error
viewer_upload = await viewer_mcp.call_tool(
"source_upload_begin",
{
"project_id": "dome-performance",
"filename": "viewer.md",
"media_type": "text/markdown",
"byte_length": 4,
"expected_sha256": hashlib.sha256(b"data").hexdigest(),
},
)
assert viewer_upload.is_error
viewer_paste = await viewer_mcp.call_tool(
"source_capture_text",
{
"capture_id": "viewer-capture-1",
"project_id": "dome-performance",
"source_key": "viewer-paste",
"text": "Viewer supplied text",
},
)
assert viewer_paste.is_error
diagnostics_result = await mcp.call_tool(
"diagnostics",
{"projects": ["dome-performance"], "include_shared": True},
)
assert not diagnostics_result.is_error
diagnostics = json.loads(_text_payload(diagnostics_result))
assert diagnostics["workflow_status"] == "not_verified"
assert diagnostics["selected_scope"]["include_shared"] is True
assert "proposal_submit" in diagnostics["authorized_operations"]
assert "run_checkpoint" in diagnostics["authorized_operations"]
assert token not in _text_payload(diagnostics_result)
run_request = {
"run_id": "mcp-run-pilot",
"objective": "Compare audience response",
"scope": {
"projects": ["dome-performance"],
"include_shared": False,
"all_projects": False,
},
"lens": "Audience behavior",
"pack_id": "gewu",
"pack_version": "0.3.0",
"pack_snapshot_digest": "a" * 64,
"module_pins": [
{
"module_id": "profile",
"implementation_version": "1.0.0",
"sha256": "b" * 64,
}
],
}
created_run = await mcp.call_tool(
"run_create", {"request_json": json.dumps(run_request)}
)
assert not created_run.is_error
run_record = json.loads(_text_payload(created_run))
assert run_record["revision"] == 0
checkpoint = {
"run_id": "mcp-run-pilot",
"expected_revision": 0,
"stage": "source-review",
}
saved_run = await mcp.call_tool(
"run_checkpoint", {"request_json": json.dumps(checkpoint)}
)
assert not saved_run.is_error
assert json.loads(_text_payload(saved_run))["revision"] == 1
read_run = await mcp.call_tool("run_status", {"run_id": "mcp-run-pilot"})
assert not read_run.is_error
assert json.loads(_text_payload(read_run))["revision"] == 1
denied = await mcp.call_tool(
"source_upload_begin",
{
"project_id": "interactive-video",
"filename": "private.md",
"media_type": "text/markdown",
"byte_length": 4,
"expected_sha256": "0" * 64,
},
)
assert denied.is_error
forbidden_capture = await mcp.call_tool(
"source_capture_url",
{
"project_id": "interactive-video",
"url": "https://research.example/source.md",
},
)
assert forbidden_capture.is_error
forbidden_paste = await mcp.call_tool(
"source_capture_text",
{
"capture_id": "out-of-scope-paste-1",
"project_id": "interactive-video",
"source_key": "out-of-scope-paste",
"text": "Must not be uploaded",
},
)
assert forbidden_paste.is_error
unallowlisted_capture = await mcp.call_tool(
"source_capture_url",
{
"project_id": "dome-performance",
"url": "https://internal.example/private",
},
)
assert unallowlisted_capture.is_error
captured = await mcp.call_tool(
"source_capture_url",
{
"project_id": "dome-performance",
"url": "https://research.example/source.md?token=do-not-echo",
},
)
assert not captured.is_error
capture_result = json.loads(_text_payload(captured))
assert capture_result["title"] == "Allowlisted fixture"
assert capture_result["media_type"] == "text/markdown"
assert "do-not-echo" not in _text_payload(captured)
assert capture_result["original_url"] == ("https://research.example/source.md")
captured_source = SourceRevisionRecord.model_validate_json(
json.dumps(capture_result["source_candidate"]), strict=True
)
assert captured_source.archive_status == "archived"
assert captured_source.final_url == "https://research.example/source.md"
assert captured_source.artifact_sha256 == capture_result["sha256"]
assert captured_source.capture_method == "url-fetch"
assert f":r:{captured_source.artifact_sha256}:" in captured_source.revision_id
pasted_text = "## Meeting note\nPasted evidence: 400 visitors.\n"
pasted_arguments = {
"capture_id": "interview-paste-1",
"project_id": "dome-performance",
"source_key": "research-interview-2026-10-14",
"text": pasted_text,
"title": "Research interview notes",
"original_url": "https://research.example/interview?token=secret",
"media_type": "text/markdown",
"source_type": "internal",
}
pasted = await mcp.call_tool("source_capture_text", pasted_arguments)
assert not pasted.is_error
pasted_retry = await mcp.call_tool("source_capture_text", pasted_arguments)
assert not pasted_retry.is_error
assert json.loads(_text_payload(pasted_retry)) == json.loads(
_text_payload(pasted)
)
changed_paste_retry = await mcp.call_tool(
"source_capture_text",
{**pasted_arguments, "text": pasted_text + "changed"},
)
assert changed_paste_retry.is_error
invalid_capture_id = await mcp.call_tool(
"source_capture_text",
{**pasted_arguments, "capture_id": "invalid capture id"},
)
assert invalid_capture_id.is_error
pasted_result = json.loads(_text_payload(pasted))
pasted_source = SourceRevisionRecord.model_validate_json(
json.dumps(pasted_result["source_candidate"]), strict=True
)
assert pasted_source.capture_method == "pasted-text"
assert pasted_source.original_url == "https://research.example/interview"
assert "secret" not in _text_payload(pasted)
capture_journal = tmp_path / "staging" / "capture-receipts"
assert all(
pasted_text.encode() not in path.read_bytes()
for path in capture_journal.glob("*.json")
)
assert (
pasted_source.artifact_sha256
== hashlib.sha256(pasted_text.encode()).hexdigest()
)
assert pasted_result["byte_length"] == len(pasted_text.encode())
assert pasted_result["extraction_id"]
pasted_extraction = artifacts.get_extraction(
pasted_result["extraction_id"],
owner_id="agent-pilot",
project_id="dome-performance",
)
assert pasted_extraction is not None
assert pasted_extraction.source_revision_id == pasted_source.revision_id
assert (
"Pasted evidence: 400 visitors."
in pasted_extraction.blocks[0].original_text
)
oversized_paste = await mcp.call_tool(
"source_capture_text",
{
"capture_id": "oversized-paste-1",
"project_id": "dome-performance",
"source_key": "oversized-paste",
"text": "x" * 1_000_001,
},
)
assert oversized_paste.is_error
control_character_paste = await mcp.call_tool(
"source_capture_text",
{
"capture_id": "invalid-paste-1",
"project_id": "dome-performance",
"source_key": "invalid-paste",
"text": "unsafe\x00content",
},
)
assert control_character_paste.is_error
oversized_author_paste = await mcp.call_tool(
"source_capture_text",
{
"capture_id": "invalid-author-paste-1",
"project_id": "dome-performance",
"source_key": "invalid-author-paste",
"text": "valid body",
"authors": ["a" * 301],
},
)
assert oversized_author_paste.is_error
changed_capture = await mcp.call_tool(
"source_capture_url",
{
"project_id": "dome-performance",
"url": "https://research.example/source.md?rev=2",
},
)
assert not changed_capture.is_error
changed_source = SourceRevisionRecord.model_validate_json(
json.dumps(json.loads(_text_payload(changed_capture))["source_candidate"]),
strict=True,
)
assert changed_source.source_id == captured_source.source_id
assert changed_source.revision_id != captured_source.revision_id
with artifacts.open_artifact(
capture_result["sha256"], project_id="dome-performance"
) as artifact:
assert artifact.read() == (
b"# Captured fixture\nAn exact source statement.\n"
)
failed_capture = await mcp.call_tool(
"source_capture_url",
{
"project_id": "dome-performance",
"url": "https://research.example/missing?access-token=must-not-leak",
"title": "Temporarily unavailable paper",
"source_type": "paper",
},
)
assert not failed_capture.is_error
failed_result = json.loads(_text_payload(failed_capture))
failed_source = SourceRevisionRecord.model_validate_json(
json.dumps(failed_result["source_candidate"]), strict=True
)
assert failed_result["status"] == "capture_failed"
assert failed_source.archive_status == "capture-failed"
assert failed_source.artifact_sha256 is None
assert "access-token" not in _text_payload(failed_capture)
failed_proposal = ProposalRevision(
proposal_id="proposal-failed-url-capture",
revision=1,
initiating_project_id="dome-performance",
scope=Scope(projects=("dome-performance",), include_shared=True),
base_commit=repository.head(),
payload_digest="0" * 64,
proposer_id="agent-pilot",
run_id="run-failed-url-capture",
operations=(
ProposalOperation(operation="register_source", source=failed_source),
),
)
failed_proposal = failed_proposal.model_copy(
update={"payload_digest": proposal_payload_digest(failed_proposal)}
)
registered_failure = await mcp.call_tool(
"proposal_submit",
{"proposal_json": failed_proposal.model_dump_json()},
)
assert not registered_failure.is_error
assert (
json.loads(_text_payload(registered_failure))["receipt"]["state"]
== "accepted"
)
captured_html = await mcp.call_tool(
"source_capture_url",
{
"project_id": "dome-performance",
"url": "https://research.example/page.html",
},
)
assert not captured_html.is_error
html_result = json.loads(_text_payload(captured_html))
assert html_result["media_type"] == "text/html"
html_extraction_result = await mcp.call_tool(
"source_extract",
{
"project_id": "dome-performance",
"artifact_sha256": html_result["sha256"],
"source_revision_id": "shared:web-page:r1",
},
)
assert not html_extraction_result.is_error
html_extraction = json.loads(_text_payload(html_extraction_result))
html_block_result = await mcp.call_tool(
"source_extraction_block",
{
"project_id": "dome-performance",
"extraction_id": html_extraction["extraction_id"],
"block_id": html_extraction["blocks"][0]["block_id"],
"offset": 0,
"length": 500,
},
)
assert not html_block_result.is_error
html_block = _text_payload(html_block_result)
assert "Exact HTML statement." in html_block
assert "ignoreInjection" not in html_block
captured_pdf = await mcp.call_tool(
"source_capture_url",
{
"project_id": "dome-performance",
"url": "https://research.example/paper.pdf",
},
)
assert not captured_pdf.is_error
pdf_result = json.loads(_text_payload(captured_pdf))
assert pdf_result["media_type"] == "application/pdf"
with artifacts.open_artifact(
pdf_result["sha256"], project_id="dome-performance"
) as artifact:
assert artifact.read() == _pdf_fixture()
pdf_extracted = await mcp.call_tool(
"source_extract",
{
"project_id": "dome-performance",
"artifact_sha256": pdf_result["sha256"],
"source_revision_id": "shared:pdf:r1",
},
)
assert not pdf_extracted.is_error
pdf_record = json.loads(_text_payload(pdf_extracted))
assert pdf_record["blocks"][0]["page"] == 1
pdf_block_result = await mcp.call_tool(
"source_extraction_block",
{
"project_id": "dome-performance",
"extraction_id": pdf_record["extraction_id"],
"block_id": pdf_record["blocks"][0]["block_id"],
"offset": 0,
"length": 500,
},
)
assert not pdf_block_result.is_error
pdf_block = json.loads(_text_payload(pdf_block_result))
assert pdf_block["page"] == 1
assert "Archived PDF evidence." in pdf_block["normalized_text"]
malformed_pdf = await mcp.call_tool(
"source_capture_url",
{
"project_id": "dome-performance",
"url": "https://research.example/malformed.pdf",
},
)
assert not malformed_pdf.is_error
malformed_result = json.loads(_text_payload(malformed_pdf))
malformed_extraction = await mcp.call_tool(
"source_extract",
{
"project_id": "dome-performance",
"artifact_sha256": malformed_result["sha256"],
"source_revision_id": malformed_result["source_candidate"][
"revision_id"
],
},
)
assert not malformed_extraction.is_error
unavailable = json.loads(_text_payload(malformed_extraction))
assert unavailable["status"] == "unavailable"
assert unavailable["error_code"] == "PDF_UNSUPPORTED"
content = BLOCK_TEXT.encode("utf-8")
digest = hashlib.sha256(content).hexdigest()
interrupted_begin = await mcp.call_tool(
"source_upload_begin",
{
"project_id": "dome-performance",
"filename": "interrupted.md",
"media_type": "text/markdown",
"byte_length": len(content),
"expected_sha256": digest,
},
)
assert not interrupted_begin.is_error
interrupted_upload = json.loads(_text_payload(interrupted_begin))
interrupted_finalize = await mcp.call_tool(
"source_upload_finalize",
{"upload_id": interrupted_upload["upload_id"]},
)
assert interrupted_finalize.is_error
uncitable = await mcp.call_tool(
"source_extract",
{
"project_id": "dome-performance",
"artifact_sha256": digest,
"source_revision_id": SOURCE_REVISION.revision_id,
},
)
assert uncitable.is_error
begun = await mcp.call_tool(
"source_upload_begin",
{
"project_id": "dome-performance",
"filename": "pilot.md",
"media_type": "text/markdown",
"byte_length": len(content),
"expected_sha256": digest,
},
)
assert not begun.is_error
upload = json.loads(_text_payload(begun))
wrong_digest_chunk = await mcp.call_tool(
"source_upload_chunk",
{
"upload_id": upload["upload_id"],
"offset": 0,
"content_base64": base64.b64encode(content).decode(),
"chunk_sha256": "0" * 64,
},
)
assert wrong_digest_chunk.is_error
chunked = await mcp.call_tool(
"source_upload_chunk",
{
"upload_id": upload["upload_id"],
"offset": 0,
"content_base64": base64.b64encode(content).decode(),
"chunk_sha256": digest,
},
)
assert not chunked.is_error
finalized = await mcp.call_tool(
"source_upload_finalize", {"upload_id": upload["upload_id"]}
)
assert not finalized.is_error
artifact = json.loads(_text_payload(finalized))
extracted = await mcp.call_tool(
"source_extract",
{
"project_id": "dome-performance",
"artifact_sha256": artifact["sha256"],
"source_revision_id": SOURCE_REVISION.revision_id,
},
)
assert not extracted.is_error
extraction_info = json.loads(_text_payload(extracted))
record = extractor.extract(
SOURCE_REVISION.revision_id, content, "text/markdown"
)
assert extraction_info["extraction_id"] == record.extraction_id
evidence = (
NODE.claims[0]
.evidence[0]
.model_copy(
update={
"extraction_id": record.extraction_id,
"locator": NODE.claims[0]
.evidence[0]
.locator.model_copy(update={"block_id": record.blocks[0].block_id}),
}
)
)
claim = NODE.claims[0].model_copy(
update={
"evidence": (evidence,),
"assessment": NODE.claims[0].assessment.model_copy(
update={"evidence_digests": (evidence_digest(evidence),)}
),
}
)
node = NODE.model_copy(update={"claims": (claim,)})
source = SOURCE_REVISION.model_copy(
update={"artifact_sha256": artifact["sha256"], "byte_length": len(content)}
)
candidate = proposal(node=node, source=source, base_commit=repository.head())
candidate = candidate.model_copy(update={"payload_digest": "0" * 64})
candidate = candidate.model_copy(
update={"payload_digest": proposal_payload_digest(candidate)}
)
wrong_location_evidence = evidence.model_copy(
update={
"quote": "400 visitors",
"locator": evidence.locator.model_copy(update={"start": 0, "end": 13}),
}
)
wrong_location_claim = claim.model_copy(
update={
"evidence": (wrong_location_evidence,),
"assessment": claim.assessment.model_copy(
update={
"evidence_digests": (evidence_digest(wrong_location_evidence),)
}
),
}
)
wrong_location_node = node.model_copy(
update={"claims": (wrong_location_claim,)}
)
wrong_location = proposal(
node=wrong_location_node,
source=source,
base_commit=repository.head(),
)
wrong_location = wrong_location.model_copy(
update={
"proposal_id": "proposal-wrong-location",
"payload_digest": "0" * 64,
}
)
wrong_location = wrong_location.model_copy(
update={"payload_digest": proposal_payload_digest(wrong_location)}
)
head_before_evaluate = repository.head()
invalid_evaluation = await mcp.call_tool(
"proposal_evaluate",
{"proposal_json": wrong_location.model_dump_json()},
)
assert not invalid_evaluation.is_error, invalid_evaluation.content
invalid_evaluation_payload = json.loads(_text_payload(invalid_evaluation))
assert invalid_evaluation_payload["status"] == "invalid"
assert "EVIDENCE_MISMATCH" in {
finding["code"] for finding in invalid_evaluation_payload["findings"]
}
assert repository.head() == head_before_evaluate
rejected = await mcp.call_tool(
"proposal_submit",
{"proposal_json": wrong_location.model_dump_json()},
)
assert not rejected.is_error, rejected.content
rejected_outcome = json.loads(_text_payload(rejected))
assert rejected_outcome["status"] == "invalid"
assert "EVIDENCE_MISMATCH" in {
finding["code"] for finding in rejected_outcome["findings"]
}
rejected_status = await mcp.call_tool(
"proposal_status",
{"proposal_id": "proposal-wrong-location", "revision": 1},
)
assert not rejected_status.is_error, rejected_status.content
rejected_status_payload = json.loads(_text_payload(rejected_status))
assert rejected_status_payload["lifecycle"] == "evaluated"
assert rejected_status_payload["validation_status"] == "invalid"
assert rejected_status_payload["receipt"] is None
assert "quote" not in _text_payload(rejected_status)
withdrawn = await mcp.call_tool(
"proposal_withdraw",
{"proposal_id": "proposal-wrong-location", "revision": 1},
)
assert not withdrawn.is_error, withdrawn.content
assert json.loads(_text_payload(withdrawn))["lifecycle"] == "withdrawn"
resubmitted = await mcp.call_tool(
"proposal_submit",
{"proposal_json": wrong_location.model_dump_json()},
)
assert resubmitted.is_error
candidate_head = repository.head()
valid_evaluation = await mcp.call_tool(
"proposal_evaluate",
{"proposal_json": candidate.model_dump_json()},
)
assert not valid_evaluation.is_error, valid_evaluation.content
valid_evaluation_payload = json.loads(_text_payload(valid_evaluation))
assert valid_evaluation_payload["status"] == "eligible"
assert valid_evaluation_payload["eligible_for_auto_accept"]
assert repository.head() == candidate_head
spoofed_assessment = claim.assessment.model_copy(
update={"assessor_id": "agent-other"}
)
spoofed_claim = claim.model_copy(update={"assessment": spoofed_assessment})
spoofed_node = node.model_copy(update={"claims": (spoofed_claim,)})
spoofed = proposal(
node=spoofed_node,
source=source,
base_commit=repository.head(),
)
spoofed = spoofed.model_copy(update={"payload_digest": "0" * 64})
spoofed = spoofed.model_copy(
update={"payload_digest": proposal_payload_digest(spoofed)}
)
spoofed_result = await mcp.call_tool(
"proposal_evaluate",
{"proposal_json": spoofed.model_dump_json()},
)
assert not spoofed_result.is_error, spoofed_result.content
assert "ASSESSOR_ATTRIBUTION_MISMATCH" in {
finding["code"]
for finding in json.loads(_text_payload(spoofed_result))["findings"]
}
unstored = await mcp.call_tool(
"proposal_status",
{"proposal_id": candidate.proposal_id, "revision": candidate.revision},
)
assert unstored.is_error
submitted = await mcp.call_tool(
"proposal_submit",
{"proposal_json": candidate.model_dump_json()},
)
assert not submitted.is_error, submitted.content
outcome = json.loads(_text_payload(submitted))
assert outcome["status"] == "accepted"
assert outcome["validation_status"] == "eligible"
assert outcome["receipt"] is not None
accepted_head = repository.head()
retried = await mcp.call_tool(
"proposal_submit",
{"proposal_json": candidate.model_dump_json()},
)
assert not retried.is_error, retried.content
retry_outcome = json.loads(_text_payload(retried))
assert retry_outcome["receipt"] == outcome["receipt"]
assert repository.head() == accepted_head
accepted_status = await mcp.call_tool(
"proposal_status",
{"proposal_id": candidate.proposal_id, "revision": candidate.revision},
)
assert not accepted_status.is_error, accepted_status.content
accepted_status_payload = json.loads(_text_payload(accepted_status))
assert accepted_status_payload["lifecycle"] == "accepted"
assert accepted_status_payload["receipt"] == outcome["receipt"]
other_token = identities.provision(
credential_id="other-agent-key",
principal_id="agent-other",
roles=("agent",),
project_ids=("dome-performance",),
)
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app),
base_url="http://localhost",
headers={"Authorization": f"Bearer {other_token}"},
) as other_http:
async with Client(
streamable_http_client("http://localhost/mcp", http_client=other_http)
) as other_mcp:
private_status = await other_mcp.call_tool(
"proposal_status",
{
"proposal_id": candidate.proposal_id,
"revision": candidate.revision,
},
)
assert private_status.is_error
impersonated_evaluation = await other_mcp.call_tool(
"proposal_evaluate",
{"proposal_json": candidate.model_dump_json()},
)
assert impersonated_evaluation.is_error
unauthorized_withdrawal = await other_mcp.call_tool(
"proposal_withdraw",
{
"proposal_id": candidate.proposal_id,
"revision": candidate.revision,
},
)
assert unauthorized_withdrawal.is_error
read = await mcp.call_tool(
"get_node",
{"project_id": node.project_id, "node_id": node.node_id},
)
assert not read.is_error
envelope = json.loads(_text_payload(read))
assert envelope["canonical_revision"] == outcome["receipt"]["commit_id"]
assert envelope["citations"][0]["quote"] == BLOCK_TEXT
selected_path = tmp_path / "operator-selected.md"
selected_bytes = b"A separately selected source for the stdio bridge.\n"
selected_path.write_bytes(selected_bytes)
@asynccontextmanager
async def remote_client() -> AsyncIterator[Client]:
async with Client(
streamable_http_client("http://localhost/mcp", http_client=http)
) as remote:
yield remote
connector = create_connector_server(
(SelectedFile("source-1", selected_path),), remote_client
)
async with Client(connector) as local:
connector_tools = await local.list_tools()
assert {tool.name for tool in connector_tools.tools} == {
"list_selected_files",
"upload_selected_file",
}
connector_result = await local.call_tool(
"upload_selected_file",
{
"file_id": "source-1",
"project_id": "dome-performance",
"source_revision_id": "dome:connector-probe:r1",
},
)
assert not connector_result.is_error
connector_text = _text_payload(connector_result)
connector_receipt = json.loads(connector_text)
assert (
connector_receipt["sha256"]
== hashlib.sha256(selected_bytes).hexdigest()
)
assert selected_bytes.decode() not in connector_text
asyncio.run(exercise())
+139
View File
@@ -0,0 +1,139 @@
import asyncio
import hashlib
import json
from pathlib import Path
import httpx2
import yaml
from mcp import Client
from mcp.client.streamable_http import streamable_http_client
from mcp.types import TextContent
from gewu.adapters.git_repository import GitCanonicalRepository
from gewu.adapters.sqlite_identity import SQLiteIdentityStore
from gewu.application.reading import CanonicalNodeReader
from gewu.bootstrap.app import create_app
from tests.domain.proposal_fixtures import NODE, SOURCE_REVISION
def test_mcp_exposes_only_authenticated_canonical_read(tmp_path: Path) -> None:
async def exercise() -> None:
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
project_key = hashlib.sha256(NODE.project_id.encode()).hexdigest()
node_key = hashlib.sha256(NODE.node_id.encode()).hexdigest()
source_key = hashlib.sha256(SOURCE_REVISION.source_id.encode()).hexdigest()
source_revision_key = hashlib.sha256(SOURCE_REVISION.revision_id.encode()).hexdigest()
repository.publish_files(
expected_head=repository.head(),
files={
f"projects/{project_key}/nodes/{node_key}.yaml": yaml.safe_dump(
NODE.model_dump(mode="json")
).encode(),
f"sources/{source_key}/{source_revision_key}.yaml": yaml.safe_dump(
SOURCE_REVISION.model_dump(mode="json")
).encode(),
},
message="Publish test node",
index_directory=tmp_path / "publishing",
)
identities = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
token = identities.provision(
credential_id="reader-key",
principal_id="research-agent",
roles=("agent",),
project_ids=(NODE.project_id,),
)
app = create_app(
identity_provider=identities,
node_reader=CanonicalNodeReader(repository),
)
async with app.router.lifespan_context(app):
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app),
base_url="http://localhost",
headers={"Authorization": f"Bearer {token}"},
) as http:
async with Client(
streamable_http_client("http://localhost/mcp", http_client=http)
) as mcp:
tools = await mcp.list_tools()
assert {tool.name for tool in tools.tools} == {
"diagnostics",
"get_node",
"node_history",
"get_source",
}
diagnostic_result = await mcp.call_tool(
"diagnostics", {"projects": [NODE.project_id]}
)
assert not diagnostic_result.is_error
diagnostic_text = diagnostic_result.content[0]
assert isinstance(diagnostic_text, TextContent)
diagnostics = json.loads(diagnostic_text.text)
assert diagnostics["transport"] == "mcp"
assert diagnostics["selected_scope"]["projects"] == [NODE.project_id]
assert diagnostics["index_state"] == "not_configured"
assert "get_node" in diagnostics["authorized_operations"]
assert "proposal_submit" not in diagnostics["authorized_operations"]
assert token not in diagnostic_text.text
denied_diagnostics = await mcp.call_tool(
"diagnostics", {"projects": ["interactive-video"]}
)
assert denied_diagnostics.is_error
result = await mcp.call_tool(
"get_node",
{"project_id": NODE.project_id, "node_id": NODE.node_id},
)
assert not result.is_error
content = result.content[0]
assert content.type == "text"
envelope = json.loads(content.text)
assert envelope["canonical_revision"] == repository.head()
assert envelope["data"][0]["node_id"] == NODE.node_id
assert envelope["citations"][0]["quote"] == NODE.claims[0].evidence[0].quote
history = await mcp.call_tool(
"node_history",
{"project_id": NODE.project_id, "node_id": NODE.node_id},
)
assert not history.is_error
assert isinstance(history.content[0], TextContent)
assert len(json.loads(history.content[0].text)["data"]) == 1
source = await mcp.call_tool(
"get_source",
{
"project_id": NODE.project_id,
"revision_id": SOURCE_REVISION.revision_id,
},
)
assert not source.is_error
assert isinstance(source.content[0], TextContent)
assert json.loads(source.content[0].text)["data"][0]["revision_id"] == (
SOURCE_REVISION.revision_id
)
assert identities.revoke("reader-key")
revoked = await mcp.call_tool(
"get_node",
{"project_id": NODE.project_id, "node_id": NODE.node_id},
)
assert revoked.is_error
revoked_message = revoked.content[0]
assert isinstance(revoked_message, TextContent)
assert NODE.node_id not in revoked_message.text
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app),
base_url="http://localhost",
) as http:
async with Client(
streamable_http_client("http://localhost/mcp", http_client=http)
) as mcp:
unauthenticated = await mcp.call_tool(
"get_node",
{"project_id": NODE.project_id, "node_id": NODE.node_id},
)
assert unauthenticated.is_error
error_message = unauthenticated.content[0]
assert isinstance(error_message, TextContent)
assert NODE.node_id not in error_message.text
asyncio.run(exercise())
+30
View File
@@ -0,0 +1,30 @@
from pathlib import Path
from typing import Any
from gewu.bootstrap.app import create_local_app
from gewu.domain.operations import OPERATIONS, Transport, operations_for
def test_http_openapi_routes_match_the_explicit_operation_allowlist(
tmp_path: Path, monkeypatch: Any
) -> None:
monkeypatch.setenv("GEWU_DATA_ROOT", str(tmp_path / "gewu-data"))
app = create_local_app()
schema = app.openapi()
exposed = {
operation["operationId"]: operation
for methods in schema["paths"].values()
for method, operation in methods.items()
if method in {"get", "post", "put", "delete", "patch"}
}
http_allowlist = {item.name for item in operations_for(Transport.HTTP)}
operation_index = {item.name: item for item in OPERATIONS}
assert set(exposed) == http_allowlist
assert "owner_decide" not in exposed
assert "proposal_submit" not in exposed
assert all(
not operation_index[name].mutates_knowledge
for name, route in exposed.items()
if "knowledge" in route.get("tags", [])
)
+227
View File
@@ -0,0 +1,227 @@
import asyncio
import hashlib
import json
import time
import httpx2
import pyotp
from cryptography.fernet import Fernet
from mcp import Client
from mcp.client.streamable_http import streamable_http_client
from gewu.adapters.git_repository import GitCanonicalRepository
from gewu.adapters.sqlite_identity import SQLiteIdentityStore
from gewu.application.reading import CanonicalNodeReader
from gewu.bootstrap.app import create_app
def test_owner_login_returns_a_short_lived_non_agent_token(tmp_path) -> None:
async def exercise() -> None:
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
identities = SQLiteIdentityStore(
tmp_path / "identity.sqlite3", owner_encryption_key=Fernet.generate_key()
)
secret = pyotp.random_base32()
identities.provision_owner(
principal_id="owner-one",
password="correct horse battery staple",
totp_secret=secret,
project_ids=("dome-performance",),
)
now = int(time.time())
identities.issue_owner_challenge(
challenge_id="challenge-one",
owner_principal_id="owner-one",
proposal_id="proposal-one",
proposal_revision=1,
payload_digest="a" * 64,
diff_digest="b" * 64,
domains=("dome-performance",),
expires_at=now + 300,
now=now,
)
agent_token = identities.provision(
credential_id="agent-key",
principal_id="agent-one",
roles=("agent",),
project_ids=("dome-performance",),
)
app = create_app(
identity_provider=identities,
owner_identity_provider=identities,
owner_service_audience="gewu-local-test",
node_reader=CanonicalNodeReader(repository),
)
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app), base_url="http://127.0.0.1"
) as client:
response = await client.post(
"/auth/owner-session",
json={
"principal_id": "owner-one",
"password": "correct horse battery staple",
"totp_code": pyotp.TOTP(secret).now(),
"challenge_id": "challenge-one",
},
)
assert response.status_code == 200, response.text
assert response.headers["cache-control"] == "no-store"
token = response.json()["session_token"]
assert token.startswith("gwo_")
assert identities.authenticate(token) is None
assert identities.authenticate(agent_token) is not None
assert (
identities.authenticate_owner_session(
token,
challenge_id="challenge-one",
audience="gewu-local-test",
)
is not None
)
wrong_origin = await client.post(
"/auth/owner-session",
headers={"Origin": "https://attacker.invalid"},
json={
"principal_id": "owner-one",
"password": "correct horse battery staple",
"totp_code": pyotp.TOTP(secret).now(),
"challenge_id": "challenge-two",
},
)
assert wrong_origin.status_code == 403
oversized = await client.post(
"/auth/owner-session",
content='{"padding":"' + ("x" * 5000) + '"}',
headers={"Content-Type": "application/json"},
)
assert oversized.status_code == 413
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app), base_url="http://gewu.example"
) as remote_http:
insecure = await remote_http.post(
"/auth/owner-session",
json={
"principal_id": "owner-one",
"password": "correct horse battery staple",
"totp_code": "000000",
"challenge_id": "challenge-three",
},
)
assert insecure.status_code == 403
asyncio.run(exercise())
def test_owner_login_is_fail_closed_without_encryption_key_or_audience(tmp_path) -> None:
async def exercise() -> None:
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
identities = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
app = create_app(
identity_provider=identities,
owner_identity_provider=identities,
node_reader=CanonicalNodeReader(repository),
)
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app), base_url="http://127.0.0.1"
) as client:
response = await client.post(
"/auth/owner-session",
json={
"principal_id": "owner-one",
"password": "correct horse battery staple",
"totp_code": "000000",
"challenge_id": "challenge-one",
},
)
assert response.status_code == 503
asyncio.run(exercise())
def test_dedicated_owner_mcp_reads_diff_and_records_decision(tmp_path) -> None:
async def exercise() -> None:
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
identities = SQLiteIdentityStore(
tmp_path / "identity.sqlite3", owner_encryption_key=Fernet.generate_key()
)
secret = pyotp.random_base32()
identities.provision_owner(
principal_id="owner-one",
password="correct horse battery staple",
totp_secret=secret,
project_ids=("dome-performance",),
)
diff_json = '{"files":[{"path":"projects/dome/nodes/a.yaml"}]}'
diff_digest = hashlib.sha256(diff_json.encode()).hexdigest()
identities.issue_owner_challenge(
challenge_id="challenge-review",
owner_principal_id="owner-one",
proposal_id="proposal-one",
proposal_revision=1,
payload_digest="a" * 64,
diff_digest=diff_digest,
domains=("dome-performance",),
expires_at=int(time.time()) + 300,
diff_json=diff_json,
)
app = create_app(
identity_provider=identities,
owner_identity_provider=identities,
owner_service_audience="gewu-owner-test",
node_reader=CanonicalNodeReader(repository),
)
async with app.router.lifespan_context(app):
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app), base_url="http://127.0.0.1"
) as login_http:
login = await login_http.post(
"/auth/owner-session",
json={
"principal_id": "owner-one",
"password": "correct horse battery staple",
"totp_code": pyotp.TOTP(secret).now(),
"challenge_id": "challenge-review",
},
)
assert login.status_code == 200, login.text
token = login.json()["session_token"]
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app),
base_url="http://localhost",
headers={"Authorization": f"Bearer {token}"},
) as owner_http:
async with Client(
streamable_http_client("http://localhost/owner-mcp", http_client=owner_http)
) as owner:
names = {tool.name for tool in (await owner.list_tools()).tools}
assert names == {
"owner_challenge_get",
"owner_diff_get",
"owner_decide",
}
challenge = await owner.call_tool(
"owner_challenge_get", {"challenge_id": "challenge-review"}
)
assert not challenge.is_error
diff = await owner.call_tool(
"owner_diff_get", {"challenge_id": "challenge-review"}
)
assert not diff.is_error
decision = await owner.call_tool(
"owner_decide",
{
"challenge_id": "challenge-review",
"expected_proposal_revision": 1,
"expected_payload_digest": "a" * 64,
"expected_diff_digest": diff_digest,
"decision": "authorize",
"rationale": "Reviewed this exact change and approve it.",
},
)
assert not decision.is_error
assert json.loads(decision.content[0].text)["status"] == "authorized"
asyncio.run(exercise())
+153
View File
@@ -0,0 +1,153 @@
import asyncio
from pathlib import Path
import httpx2
from gewu.adapters.git_repository import GitCanonicalRepository
from gewu.adapters.sqlite_identity import SQLiteIdentityStore
from gewu.application.reading import CanonicalNodeReader
from gewu.application.registry import LocalProjectRegistry
from gewu.bootstrap.app import create_app
from gewu.domain.contracts import AutoAcceptPolicy, ProjectRecord, ProjectStatus
def test_project_registry_lifecycle_is_admin_only_and_durable(tmp_path: Path) -> None:
async def exercise() -> None:
identities = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
admin_token = identities.provision(
credential_id="project-admin",
principal_id="owner-operator",
roles=("admin",),
)
viewer_token = identities.provision(
credential_id="project-viewer",
principal_id="research-reader",
roles=("viewer",),
)
mixed_agent_admin_token = identities.provision(
credential_id="mixed-agent-admin",
principal_id="agent-with-admin-role",
roles=("admin", "agent"),
)
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
registry = LocalProjectRegistry(
tmp_path / "control" / "registry.json",
repository=repository,
index_directory=tmp_path / "publishing",
authority_lock_path=tmp_path / "workflow" / "locks" / "canonical-writer.lock",
)
app = create_app(
identity_provider=identities,
node_reader=CanonicalNodeReader(repository),
project_registry=registry,
)
project = ProjectRecord(
project_id="new-research",
title="New research",
status=ProjectStatus.ACTIVE,
description="An administrator-owned research lens.",
owners=("owner-operator",),
north_star="Investigate a new research area.",
key_questions=("What is known?",),
autonomy_policy_id="new-policy",
)
policy = AutoAcceptPolicy(
policy_id="new-policy",
policy_version=1,
project_id="new-research",
require_supported_assessment=True,
)
body = {
"project": project.model_dump(mode="json"),
"policy": policy.model_dump(mode="json"),
}
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app), base_url="http://test"
) as http:
assert (await http.get("/v1/admin/projects")).status_code == 401
denied = await http.post(
"/v1/admin/projects",
json=body,
headers={"Authorization": f"Bearer {viewer_token}"},
)
assert denied.status_code == 403
mixed_agent_denied = await http.post(
"/v1/admin/projects",
json=body,
headers={"Authorization": f"Bearer {mixed_agent_admin_token}"},
)
assert mixed_agent_denied.status_code == 403
oversized = await http.post(
"/v1/admin/projects",
content=b" " * (64 * 1024 + 1),
headers={
"Authorization": f"Bearer {admin_token}",
"Content-Type": "application/json",
},
)
assert oversized.status_code == 413
headers = {"Authorization": f"Bearer {admin_token}"}
diagnostics = await http.get("/v1/diagnostics", headers=headers)
assert diagnostics.status_code == 200
assert {
"admin_list_projects",
"project_create",
"project_archive",
}.issubset(diagnostics.json()["authorized_operations"])
created = await http.post("/v1/admin/projects", json=body, headers=headers)
assert created.status_code == 201, created.text
assert created.json()["project_id"] == "new-research"
listing = await http.get("/v1/admin/projects", headers=headers)
assert [item["project_id"] for item in listing.json()] == ["new-research"]
duplicate = await http.post("/v1/admin/projects", json=body, headers=headers)
assert duplicate.status_code == 409
archived = await http.post("/v1/admin/projects/new-research/archive", headers=headers)
assert archived.status_code == 200
assert archived.json()["status"] == "archived"
assert registry.records()[0].status == ProjectStatus.ARCHIVED
session = identities.create_browser_session(admin_token)
browser_project = project.model_copy(
update={
"project_id": "browser-created",
"title": "Browser created",
"autonomy_policy_id": "browser-policy",
}
)
browser_policy = policy.model_copy(
update={"policy_id": "browser-policy", "project_id": "browser-created"}
)
browser_body = {
"project": browser_project.model_dump(mode="json"),
"policy": browser_policy.model_dump(mode="json"),
}
cookie = {"Cookie": f"gewu_session={session.session_token}"}
without_csrf = await http.post(
"/v1/admin/projects",
json=browser_body,
headers={**cookie, "Origin": "http://test"},
)
assert without_csrf.status_code == 403
wrong_origin = await http.post(
"/v1/admin/projects",
json=browser_body,
headers={
**cookie,
"Origin": "https://attacker.example",
"X-GEWU-CSRF": session.csrf_token,
},
)
assert wrong_origin.status_code == 403
browser_created = await http.post(
"/v1/admin/projects",
json=browser_body,
headers={
**cookie,
"Origin": "http://test",
"X-GEWU-CSRF": session.csrf_token,
},
)
assert browser_created.status_code == 201, browser_created.text
asyncio.run(exercise())
+439
View File
@@ -0,0 +1,439 @@
import asyncio
import hashlib
import json
from pathlib import Path
import httpx2
import yaml
from mcp import Client
from mcp.client.streamable_http import streamable_http_client
from mcp.types import TextContent
from gewu.adapters.git_repository import GitCanonicalRepository
from gewu.adapters.sqlite_identity import SQLiteIdentityStore
from gewu.adapters.sqlite_search import SQLiteSearchIndex
from gewu.application.reading import CanonicalNodeReader
from gewu.application.search import CanonicalSearchService
from gewu.bootstrap.app import create_app
from gewu.domain.contracts import EdgeEndpoint, Scope, SearchRequest, TypedEdge
from gewu.domain.validation import evidence_digest
from tests.domain.proposal_fixtures import CLAIM, EVIDENCE, NODE, SOURCE_REVISION
def _canonical_path(prefix: str, stable_id: str) -> str:
key = hashlib.sha256(stable_id.encode()).hexdigest()
return f"{prefix}/{key}.yaml"
def test_project_include_resolution_across_http_mcp_and_direct_reads(tmp_path: Path) -> None:
async def exercise() -> None:
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
dome_node = NODE.model_copy(
update={
"node_id": "dome:scope-default-node",
"summary": "scopeprobe dome project record",
}
)
video_source = SOURCE_REVISION.model_copy(
update={
"source_id": "video:scope-source",
"revision_id": "video:scope-source:r1",
"title": "Video-only scope source",
"ingested_for": ("interactive-video",),
"artifact_sha256": None,
"media_type": "application/octet-stream",
"byte_length": 0,
"original_url": "https://video.example/scope-source",
"archive_status": "reference-only",
}
)
video_evidence = EVIDENCE.model_copy(
update={"source_revision_id": video_source.revision_id}
)
video_assessment = CLAIM.assessment.model_copy(
update={"evidence_digests": (evidence_digest(video_evidence),)}
)
video_claim = CLAIM.model_copy(
update={"evidence": (video_evidence,), "assessment": video_assessment}
)
video_node = NODE.model_copy(
update={
"project_id": "interactive-video",
"node_id": "video:scope-default-node",
"title": "Video scope fixture",
"summary": "scopeprobe interactive video project record",
"claims": (video_claim,),
}
)
edge = TypedEdge(
edge_id="scope:cross-project-edge",
edge_type="compared_with",
source=EdgeEndpoint(kind="node", id=dome_node.node_id),
target=EdgeEndpoint(kind="node", id=video_node.node_id),
)
files = {
f"projects/{hashlib.sha256(dome_node.project_id.encode()).hexdigest()}/nodes/"
f"{hashlib.sha256(dome_node.node_id.encode()).hexdigest()}.yaml": yaml.safe_dump(
dome_node.model_dump(mode="json")
).encode(),
f"projects/{hashlib.sha256(video_node.project_id.encode()).hexdigest()}/nodes/"
f"{hashlib.sha256(video_node.node_id.encode()).hexdigest()}.yaml": yaml.safe_dump(
video_node.model_dump(mode="json")
).encode(),
(
f"sources/{hashlib.sha256(video_source.source_id.encode()).hexdigest()}/"
f"{hashlib.sha256(video_source.revision_id.encode()).hexdigest()}.yaml"
): yaml.safe_dump(video_source.model_dump(mode="json")).encode(),
_canonical_path("edges", edge.edge_id): yaml.safe_dump(
edge.model_dump(mode="json")
).encode(),
}
repository.publish_files(
expected_head=repository.head(),
files=files,
message="Add project-scope fixtures",
index_directory=tmp_path / "publishing",
)
identities = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
all_scope_token = identities.provision(
credential_id="all-scope-reader",
principal_id="all-scope-reader",
roles=("viewer",),
project_ids=("dome-performance", "interactive-video", "shared"),
)
dome_only_token = identities.provision(
credential_id="dome-only-reader",
principal_id="dome-only-reader",
roles=("viewer",),
project_ids=("dome-performance", "shared"),
)
search_index = SQLiteSearchIndex(tmp_path / "search.sqlite3", repository)
indexed_nodes = search_index.canonical_nodes()[1]
assert {node.node_id for node in indexed_nodes} == {dome_node.node_id, video_node.node_id}
lexical_scope_results = search_index.search(
query="scopeprobe",
project_ids=("dome-performance", "interactive-video"),
include_shared=False,
limit=10,
)
assert len(lexical_scope_results.nodes) == 2, [
item.node.summary for item in lexical_scope_results.nodes
]
search = CanonicalSearchService(
index=search_index,
project_ids_provider=lambda: ("dome-performance", "interactive-video"),
canonical_head=repository.head,
default_includes_provider=lambda project_id: (
("interactive-video", "shared") if project_id == "dome-performance" else ()
),
)
app = create_app(
identity_provider=identities,
node_reader=CanonicalNodeReader(repository),
search_service=search,
)
direct_service_result = search.search(
SearchRequest(
query="scopeprobe",
scope=Scope(projects=("dome-performance",), include_shared=False),
limit=10,
budget_tokens=50000,
),
principal=identities.authenticate(all_scope_token),
)
assert len(direct_service_result.data) == 2
async with app.router.lifespan_context(app):
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app), base_url="http://localhost"
) as http:
default = await http.post(
"/v1/search",
json={
"query": "scopeprobe",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"limit": 10,
"budget_tokens": 50000,
},
headers={"Authorization": f"Bearer {all_scope_token}"},
)
assert default.status_code == 200, default.text
assert {item["node"]["project_id"] for item in default.json()["data"]} == {
"dome-performance",
"interactive-video",
}, default.json()
assert video_source.revision_id in {
item["source_revision_id"] for item in default.json()["citations"]
}
explicit_empty = await http.post(
"/v1/search",
json={
"query": "scopeprobe",
"scope": {
"projects": ["dome-performance"],
"include_projects": [],
"include_shared": False,
},
"limit": 10,
"budget_tokens": 50000,
},
headers={"Authorization": f"Bearer {all_scope_token}"},
)
assert explicit_empty.status_code == 200, explicit_empty.text
assert {item["node"]["project_id"] for item in explicit_empty.json()["data"]} == {
"dome-performance"
}
assert video_source.revision_id not in {
item["source_revision_id"] for item in explicit_empty.json()["citations"]
}
explicit_include = await http.post(
"/v1/search",
json={
"query": "scopeprobe",
"scope": {
"projects": ["dome-performance"],
"include_projects": ["interactive-video"],
"include_shared": False,
},
"limit": 10,
"budget_tokens": 50000,
},
headers={"Authorization": f"Bearer {all_scope_token}"},
)
assert explicit_include.status_code == 200, explicit_include.text
assert len(explicit_include.json()["data"]) == 2
denied_default = await http.post(
"/v1/search",
json={
"query": "scopeprobe",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"limit": 10,
"budget_tokens": 50000,
},
headers={"Authorization": f"Bearer {dome_only_token}"},
)
assert denied_default.status_code == 403
denied_explicit_include = await http.post(
"/v1/search",
json={
"query": "scopeprobe",
"scope": {
"projects": ["dome-performance"],
"include_projects": ["interactive-video"],
"include_shared": False,
},
"limit": 10,
"budget_tokens": 50000,
},
headers={"Authorization": f"Bearer {dome_only_token}"},
)
assert denied_explicit_include.status_code == 403
allowed_empty = await http.post(
"/v1/search",
json={
"query": "scopeprobe",
"scope": {
"projects": ["dome-performance"],
"include_projects": [],
"include_shared": False,
},
"limit": 10,
"budget_tokens": 50000,
},
headers={"Authorization": f"Bearer {dome_only_token}"},
)
assert allowed_empty.status_code == 200
default_neighbors = await http.post(
"/v1/neighbors",
json={
"node_id": dome_node.node_id,
"scope": {"projects": ["dome-performance"], "include_shared": False},
"limit": 10,
"budget_tokens": 50000,
},
headers={"Authorization": f"Bearer {all_scope_token}"},
)
assert default_neighbors.status_code == 200, default_neighbors.text
assert default_neighbors.json()["data"][0]["neighbor"]["id"] == video_node.node_id
restricted_neighbors = await http.post(
"/v1/neighbors",
json={
"node_id": dome_node.node_id,
"scope": {
"projects": ["dome-performance"],
"include_projects": [],
"include_shared": False,
},
"limit": 10,
"budget_tokens": 50000,
},
headers={"Authorization": f"Bearer {all_scope_token}"},
)
assert restricted_neighbors.status_code == 200
assert restricted_neighbors.json()["data"] == []
default_sources = await http.post(
"/v1/sources",
json={
"scope": {"projects": ["dome-performance"], "include_shared": False},
"limit": 10,
"budget_tokens": 50000,
},
headers={"Authorization": f"Bearer {all_scope_token}"},
)
assert default_sources.status_code == 200, default_sources.text
assert video_source.revision_id in {
item["source"]["revision_id"] for item in default_sources.json()["data"]
}
explicit_empty_sources = await http.post(
"/v1/sources",
json={
"scope": {
"projects": ["dome-performance"],
"include_projects": [],
"include_shared": False,
},
"limit": 10,
"budget_tokens": 50000,
},
headers={"Authorization": f"Bearer {all_scope_token}"},
)
assert explicit_empty_sources.status_code == 200
assert video_source.revision_id not in {
item["source"]["revision_id"] for item in explicit_empty_sources.json()["data"]
}
direct_node_denied = await http.get(
f"/v1/projects/interactive-video/nodes/{video_node.node_id}",
headers={"Authorization": f"Bearer {dome_only_token}"},
)
assert direct_node_denied.status_code == 403
direct_source_denied = await http.get(
f"/v1/projects/interactive-video/sources/{video_source.revision_id}",
headers={"Authorization": f"Bearer {dome_only_token}"},
)
assert direct_source_denied.status_code == 403
direct_node_allowed = await http.get(
f"/v1/projects/interactive-video/nodes/{video_node.node_id}",
headers={"Authorization": f"Bearer {all_scope_token}"},
)
assert direct_node_allowed.status_code == 200
direct_source_allowed = await http.get(
f"/v1/projects/interactive-video/sources/{video_source.revision_id}",
headers={"Authorization": f"Bearer {all_scope_token}"},
)
assert direct_source_allowed.status_code == 200
http.headers["Authorization"] = f"Bearer {all_scope_token}"
async with Client(
streamable_http_client("http://localhost/mcp", http_client=http)
) as mcp:
mcp_default = await mcp.call_tool(
"search",
{
"query": "scopeprobe",
"projects": ["dome-performance"],
"include_shared": False,
"all_projects": False,
"limit": 10,
"budget_tokens": 50000,
},
)
assert not mcp_default.is_error
assert isinstance(mcp_default.content[0], TextContent)
default_payload = json.loads(mcp_default.content[0].text)
assert len(default_payload["data"]) == 2
mcp_default_sources = await mcp.call_tool(
"list_sources",
{
"projects": ["dome-performance"],
"include_shared": False,
"all_projects": False,
"limit": 10,
"budget_tokens": 50000,
},
)
assert not mcp_default_sources.is_error
assert isinstance(mcp_default_sources.content[0], TextContent)
assert video_source.revision_id in {
row["source"]["revision_id"]
for row in json.loads(mcp_default_sources.content[0].text)["data"]
}
mcp_default_neighbors = await mcp.call_tool(
"neighbors",
{
"node_id": dome_node.node_id,
"projects": ["dome-performance"],
"include_shared": False,
"all_projects": False,
"edge_types": [],
"limit": 10,
"budget_tokens": 50000,
},
)
assert not mcp_default_neighbors.is_error
assert isinstance(mcp_default_neighbors.content[0], TextContent)
assert (
json.loads(mcp_default_neighbors.content[0].text)["data"][0]["neighbor"][
"id"
]
== video_node.node_id
)
mcp_explicit_empty = await mcp.call_tool(
"search",
{
"query": "scopeprobe",
"projects": ["dome-performance"],
"include_projects": [],
"include_shared": False,
"all_projects": False,
"limit": 10,
"budget_tokens": 50000,
},
)
assert not mcp_explicit_empty.is_error
assert isinstance(mcp_explicit_empty.content[0], TextContent)
assert len(json.loads(mcp_explicit_empty.content[0].text)["data"]) == 1
mcp_empty_sources = await mcp.call_tool(
"list_sources",
{
"projects": ["dome-performance"],
"include_projects": [],
"include_shared": False,
"all_projects": False,
"limit": 10,
"budget_tokens": 50000,
},
)
assert not mcp_empty_sources.is_error
assert isinstance(mcp_empty_sources.content[0], TextContent)
assert video_source.revision_id not in {
row["source"]["revision_id"]
for row in json.loads(mcp_empty_sources.content[0].text)["data"]
}
mcp_empty_neighbors = await mcp.call_tool(
"neighbors",
{
"node_id": dome_node.node_id,
"projects": ["dome-performance"],
"include_projects": [],
"include_shared": False,
"all_projects": False,
"edge_types": [],
"limit": 10,
"budget_tokens": 50000,
},
)
assert not mcp_empty_neighbors.is_error
assert isinstance(mcp_empty_neighbors.content[0], TextContent)
assert json.loads(mcp_empty_neighbors.content[0].text)["data"] == []
asyncio.run(exercise())
+616
View File
@@ -0,0 +1,616 @@
import asyncio
import hashlib
import json
from datetime import UTC, datetime
from pathlib import Path
import httpx2
import yaml
from mcp import Client
from mcp.client.streamable_http import streamable_http_client
from mcp.types import TextContent
from gewu.adapters.git_repository import GitCanonicalRepository
from gewu.adapters.sqlite_identity import SQLiteIdentityStore
from gewu.adapters.sqlite_search import SQLiteSearchIndex
from gewu.application.reading import CanonicalNodeReader
from gewu.application.search import CanonicalSearchService
from gewu.bootstrap.app import create_app
from gewu.domain.contracts import EdgeEndpoint, NodeType, TypedEdge
from tests.domain.proposal_fixtures import NODE, PROJECTS, SOURCE_REVISION
def test_authenticated_search_scope_through_http_and_mcp(tmp_path: Path) -> None:
async def exercise() -> None:
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
project_key = hashlib.sha256(NODE.project_id.encode()).hexdigest()
node_key = hashlib.sha256(NODE.node_id.encode()).hexdigest()
source_key = hashlib.sha256(SOURCE_REVISION.revision_id.encode()).hexdigest()
stale_node = NODE.model_copy(
update={
"as_of": datetime(2024, 1, 1, tzinfo=UTC),
"stale_after": datetime(2025, 1, 1, tzinfo=UTC),
}
)
problem_node = NODE.model_copy(
update={
"node_id": "dome:problem-browse-fixture",
"node_type": NodeType.PROBLEM,
"title": "Problem browse fixture",
"value": None,
"unit": None,
}
)
problem_key = hashlib.sha256(problem_node.node_id.encode()).hexdigest()
older_source = SOURCE_REVISION.model_copy(
update={
"source_id": "dome:older-pilot-report",
"revision_id": "dome:older-pilot-report:r1",
"title": "Older pilot installation report",
"captured_at": datetime(2025, 1, 1, tzinfo=UTC),
}
)
related = NODE.model_copy(update={"node_id": "z:dome-related"})
related_key = hashlib.sha256(related.node_id.encode()).hexdigest()
related_two = NODE.model_copy(update={"node_id": "zz:dome-related-two"})
related_two_key = hashlib.sha256(related_two.node_id.encode()).hexdigest()
edge = TypedEdge(
edge_id="dome:related-edge",
edge_type="compared_with",
source=EdgeEndpoint(kind="node", id=NODE.node_id),
target=EdgeEndpoint(kind="node", id=related.node_id),
)
edge_two = TypedEdge(
edge_id="dome:related-edge-two",
edge_type="compared_with",
source=EdgeEndpoint(kind="node", id=NODE.node_id),
target=EdgeEndpoint(kind="node", id=related_two.node_id),
)
repository.publish_files(
expected_head=repository.head(),
files={
f"projects/{project_key}/nodes/{node_key}.yaml": yaml.safe_dump(
stale_node.model_dump(mode="json")
).encode(),
f"projects/{project_key}/nodes/{problem_key}.yaml": yaml.safe_dump(
problem_node.model_dump(mode="json")
).encode(),
f"projects/{project_key}/nodes/{related_key}.yaml": yaml.safe_dump(
related.model_dump(mode="json")
).encode(),
f"projects/{project_key}/nodes/{related_two_key}.yaml": yaml.safe_dump(
related_two.model_dump(mode="json")
).encode(),
(
f"sources/{hashlib.sha256(SOURCE_REVISION.source_id.encode()).hexdigest()}/"
f"{source_key}.yaml"
): yaml.safe_dump(SOURCE_REVISION.model_dump(mode="json")).encode(),
(
f"sources/{hashlib.sha256(older_source.source_id.encode()).hexdigest()}/"
f"{hashlib.sha256(older_source.revision_id.encode()).hexdigest()}.yaml"
): yaml.safe_dump(older_source.model_dump(mode="json")).encode(),
f"edges/{hashlib.sha256(edge.edge_id.encode()).hexdigest()}.yaml": yaml.safe_dump(
edge.model_dump(mode="json")
).encode(),
(
f"edges/{hashlib.sha256(edge_two.edge_id.encode()).hexdigest()}.yaml"
): yaml.safe_dump(edge_two.model_dump(mode="json")).encode(),
},
message="Add searchable node",
index_directory=tmp_path / "publishing",
)
identities = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
token = identities.provision(
credential_id="search-key",
principal_id="search-reader",
roles=("viewer",),
project_ids=("dome-performance",),
)
search = CanonicalSearchService(
index=SQLiteSearchIndex(tmp_path / "search.sqlite3", repository),
project_ids_provider=lambda: PROJECTS.keys(),
canonical_head=repository.head,
)
app = create_app(
identity_provider=identities,
node_reader=CanonicalNodeReader(repository),
search_service=search,
)
async with app.router.lifespan_context(app):
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app),
base_url="http://localhost",
headers={"Authorization": f"Bearer {token}"},
) as http:
diagnostic_unbuilt = await http.get("/v1/diagnostics?projects=dome-performance")
assert diagnostic_unbuilt.status_code == 200
unbuilt_state = diagnostic_unbuilt.json()
assert unbuilt_state["index_state"] == "not_built"
assert unbuilt_state["index_revision"] is None
assert unbuilt_state["index_schema_version"] == 4
result = await http.post(
"/v1/search",
json={
"query": "400 visitors",
"scope": {
"projects": ["dome-performance"],
"include_shared": False,
},
"limit": 10,
"budget_tokens": 5000,
},
)
assert result.status_code == 200, result.text
envelope = result.json()
assert envelope["retrieval_mode"] == "keyword"
assert envelope["index_revision"] == repository.head()
assert envelope["data"][0]["node"]["node_id"] == NODE.node_id
assert envelope["citations"][0]["quote"] == NODE.claims[0].evidence[0].quote
listed_sources = await http.post(
"/v1/sources",
json={
"scope": {"projects": ["dome-performance"], "include_shared": False},
"limit": 10,
"budget_tokens": 5000,
},
)
assert listed_sources.status_code == 200, listed_sources.text
assert listed_sources.json()["data"][0]["source"]["revision_id"] == (
SOURCE_REVISION.revision_id
)
source_page_one = await http.post(
"/v1/sources",
json={
"scope": {"projects": ["dome-performance"], "include_shared": False},
"limit": 1,
"budget_tokens": 5000,
},
)
source_cursor = source_page_one.json()["next_cursor"]
assert source_cursor
source_page_two = await http.post(
"/v1/sources",
json={
"scope": {"projects": ["dome-performance"], "include_shared": False},
"limit": 1,
"cursor": source_cursor,
"budget_tokens": 5000,
},
)
assert source_page_two.status_code == 200
assert source_page_two.json()["data"][0]["source"]["revision_id"] == (
older_source.revision_id
)
denied_sources = await http.post(
"/v1/sources",
json={
"scope": {"projects": ["interactive-video"], "include_shared": False},
"limit": 10,
"budget_tokens": 5000,
},
)
assert denied_sources.status_code == 403
problem_filter = await http.post(
"/v1/search",
json={
"query": "unmatched phrase",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"node_types": ["problem"],
"budget_tokens": 5000,
},
)
assert problem_filter.status_code == 200, problem_filter.text
assert problem_filter.json()["data"] == []
problem_browse = await http.post(
"/v1/search",
json={
"query": "",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"node_types": ["problem"],
"budget_tokens": 5000,
},
)
assert problem_browse.status_code == 200, problem_browse.text
assert [row["node"]["node_id"] for row in problem_browse.json()["data"]] == [
problem_node.node_id
]
unbounded_browse = await http.post(
"/v1/search",
json={
"query": "",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"budget_tokens": 5000,
},
)
assert unbounded_browse.status_code == 422
stale_filter = await http.post(
"/v1/search",
json={
"query": "400 visitors",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"stale_only": True,
"budget_tokens": 5000,
},
)
assert stale_filter.status_code == 200, stale_filter.text
assert stale_filter.json()["data"][0]["node"]["node_id"] == NODE.node_id
facet_filter = await http.post(
"/v1/search",
json={
"query": "400 visitors",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"tags": ["attendance"],
"audience": ["product"],
"use_for": ["comparison"],
"domain": ["production"],
"budget_tokens": 5000,
},
)
assert facet_filter.status_code == 200, facet_filter.text
assert facet_filter.json()["data"][0]["node"]["node_id"] == NODE.node_id
unmatched_facet = await http.post(
"/v1/search",
json={
"query": "400 visitors",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"domain": ["unrelated-domain"],
"budget_tokens": 5000,
},
)
assert unmatched_facet.status_code == 200, unmatched_facet.text
assert unmatched_facet.json()["data"] == []
diagnostic_current = await http.get("/v1/diagnostics?projects=dome-performance")
assert diagnostic_current.status_code == 200
current_state = diagnostic_current.json()
assert current_state["index_state"] == "current"
assert current_state["index_revision"] == repository.head()
assert "proposal_submit" not in current_state["authorized_operations"]
projects = await http.get("/v1/projects")
assert projects.status_code == 200
assert projects.json() == ["dome-performance"]
page_one = await http.post(
"/v1/search",
json={
"query": "visitors",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"limit": 1,
"budget_tokens": 5000,
},
)
assert page_one.status_code == 200
cursor = page_one.json()["next_cursor"]
assert cursor
page_two = await http.post(
"/v1/search",
json={
"query": "visitors",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"limit": 1,
"cursor": cursor,
"budget_tokens": 5000,
},
)
assert page_two.status_code == 200
assert (
page_two.json()["data"][0]["node"]["node_id"]
!= page_one.json()["data"][0]["node"]["node_id"]
)
reused_cursor = await http.post(
"/v1/search",
json={
"query": "400 visitors",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"limit": 1,
"cursor": cursor,
"budget_tokens": 5000,
},
)
assert reused_cursor.status_code == 422
changed_facet_cursor = await http.post(
"/v1/search",
json={
"query": "visitors",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"node_types": ["problem"],
"limit": 1,
"cursor": cursor,
"budget_tokens": 5000,
},
)
assert changed_facet_cursor.status_code == 422
changed_tag_cursor = await http.post(
"/v1/search",
json={
"query": "visitors",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"tags": ["attendance"],
"limit": 1,
"cursor": cursor,
"budget_tokens": 5000,
},
)
assert changed_tag_cursor.status_code == 422
adjacent = await http.post(
"/v1/neighbors",
json={
"node_id": NODE.node_id,
"scope": {"projects": ["dome-performance"], "include_shared": False},
"limit": 1,
"budget_tokens": 5000,
},
)
assert adjacent.status_code == 200, adjacent.text
assert adjacent.json()["data"][0]["neighbor"]["id"] == related.node_id
neighbor_cursor = adjacent.json()["next_cursor"]
assert neighbor_cursor
adjacent_two = await http.post(
"/v1/neighbors",
json={
"node_id": NODE.node_id,
"scope": {"projects": ["dome-performance"], "include_shared": False},
"limit": 1,
"cursor": neighbor_cursor,
"budget_tokens": 5000,
},
)
assert adjacent_two.status_code == 200
assert adjacent_two.json()["data"][0]["neighbor"]["id"] == related_two.node_id
assert adjacent_two.json()["next_cursor"] is None
assert {citation["quote"] for citation in adjacent.json()["citations"]} == {
NODE.claims[0].evidence[0].quote
}
newer = NODE.model_copy(update={"node_id": "new:unrelated-revision-node"})
newer_key = hashlib.sha256(newer.node_id.encode()).hexdigest()
repository.publish_files(
expected_head=repository.head(),
files={
f"projects/{project_key}/nodes/{newer_key}.yaml": yaml.safe_dump(
newer.model_dump(mode="json")
).encode()
},
message="Advance canonical index for cursor-expiry check",
index_directory=tmp_path / "publishing",
)
diagnostic_lag = await http.get("/v1/diagnostics?projects=dome-performance")
assert diagnostic_lag.status_code == 200
lag_state = diagnostic_lag.json()
assert lag_state["index_state"] == "lagging"
assert lag_state["index_revision"] != repository.head()
assert lag_state["canonical_revision"] == repository.head()
expired_search = await http.post(
"/v1/search",
json={
"query": "visitors",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"limit": 1,
"cursor": cursor,
"budget_tokens": 5000,
},
)
assert expired_search.status_code == 409
expired_neighbors = await http.post(
"/v1/neighbors",
json={
"node_id": NODE.node_id,
"scope": {"projects": ["dome-performance"], "include_shared": False},
"limit": 1,
"cursor": neighbor_cursor,
"budget_tokens": 5000,
},
)
assert expired_neighbors.status_code == 409
too_small = await http.post(
"/v1/search",
json={
"query": "visitors",
"scope": {
"projects": ["dome-performance"],
"include_shared": False,
},
"budget_tokens": 256,
},
)
assert too_small.status_code == 413
dated_confidence_search = await http.post(
"/v1/search",
json={
"query": "visitors",
"scope": {
"projects": ["dome-performance"],
"include_shared": False,
},
"confidences": ["medium"],
"as_of_from": "2024-01-01",
"as_of_to": "2024-12-31",
"budget_tokens": 5000,
},
)
assert dated_confidence_search.status_code == 200, dated_confidence_search.text
assert [
row["node"]["node_id"] for row in dated_confidence_search.json()["data"]
] == [NODE.node_id]
filtered_page = await http.post(
"/v1/search",
json={
"query": "visitors",
"scope": {
"projects": ["dome-performance"],
"include_shared": False,
},
"confidences": ["medium"],
"limit": 1,
"budget_tokens": 5000,
},
)
assert filtered_page.status_code == 200
filter_cursor = filtered_page.json()["next_cursor"]
assert filter_cursor
changed_filter_page = await http.post(
"/v1/search",
json={
"query": "visitors",
"scope": {
"projects": ["dome-performance"],
"include_shared": False,
},
"confidences": ["low"],
"limit": 1,
"cursor": filter_cursor,
"budget_tokens": 5000,
},
)
assert changed_filter_page.status_code == 422
forbidden = await http.post(
"/v1/search",
json={
"query": "visitors",
"scope": {
"projects": ["interactive-video"],
"include_shared": False,
},
"budget_tokens": 5000,
},
)
assert forbidden.status_code == 403
async with Client(
streamable_http_client("http://localhost/mcp", http_client=http)
) as mcp:
tools = await mcp.list_tools()
assert {tool.name for tool in tools.tools} == {
"diagnostics",
"get_node",
"node_history",
"get_source",
"list_projects",
"search",
"neighbors",
"list_sources",
}
listed = await mcp.call_tool("list_projects", {})
assert not listed.is_error
assert isinstance(listed.content[0], TextContent)
assert json.loads(listed.content[0].text) == ["dome-performance"]
searched = await mcp.call_tool(
"search",
{
"query": "400 visitors",
"projects": ["dome-performance"],
"include_shared": False,
"all_projects": False,
"limit": 10,
"budget_tokens": 5000,
},
)
assert not searched.is_error
assert isinstance(searched.content[0], TextContent)
assert (
json.loads(searched.content[0].text)["data"][0]["node"]["node_id"]
== NODE.node_id
)
dated_confidence_tool = await mcp.call_tool(
"search",
{
"query": "visitors",
"projects": ["dome-performance"],
"include_shared": False,
"all_projects": False,
"limit": 10,
"budget_tokens": 5000,
"confidences": ["medium"],
"as_of_from": "2024-01-01",
"as_of_to": "2024-12-31",
},
)
assert not dated_confidence_tool.is_error
assert [
row["node"]["node_id"]
for row in json.loads(dated_confidence_tool.content[0].text)["data"]
] == [NODE.node_id]
filtered_search = await mcp.call_tool(
"search",
{
"query": "unmatched phrase",
"projects": ["dome-performance"],
"include_shared": False,
"all_projects": False,
"limit": 10,
"budget_tokens": 5000,
"node_types": ["problem"],
},
)
assert not filtered_search.is_error
assert json.loads(filtered_search.content[0].text)["data"] == []
browsed_problems = await mcp.call_tool(
"search",
{
"query": "",
"projects": ["dome-performance"],
"include_shared": False,
"all_projects": False,
"limit": 10,
"budget_tokens": 5000,
"node_types": ["problem"],
},
)
assert not browsed_problems.is_error
assert [
row["node"]["node_id"]
for row in json.loads(browsed_problems.content[0].text)["data"]
] == [problem_node.node_id]
facet_search = await mcp.call_tool(
"search",
{
"query": "400 visitors",
"projects": ["dome-performance"],
"include_shared": False,
"all_projects": False,
"limit": 10,
"budget_tokens": 5000,
"tags": ["attendance"],
"audience": ["product"],
"use_for": ["comparison"],
"domain": ["production"],
},
)
assert not facet_search.is_error
assert (
json.loads(facet_search.content[0].text)["data"][0]["node"]["node_id"]
== NODE.node_id
)
listed_sources_tool = await mcp.call_tool(
"list_sources",
{
"projects": ["dome-performance"],
"include_shared": False,
"all_projects": False,
"limit": 10,
"budget_tokens": 5000,
},
)
assert not listed_sources_tool.is_error
assert (
json.loads(listed_sources_tool.content[0].text)["data"][0]["source"][
"revision_id"
]
== SOURCE_REVISION.revision_id
)
adjacent_tool = await mcp.call_tool(
"neighbors",
{
"node_id": NODE.node_id,
"projects": ["dome-performance"],
"include_shared": False,
"all_projects": False,
"edge_types": [],
"limit": 10,
"budget_tokens": 5000,
},
)
assert not adjacent_tool.is_error
assert isinstance(adjacent_tool.content[0], TextContent)
assert (
json.loads(adjacent_tool.content[0].text)["data"][0]["neighbor"]["id"]
== related.node_id
)
asyncio.run(exercise())
+374
View File
@@ -0,0 +1,374 @@
import asyncio
import base64
import hashlib
import json
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from pathlib import Path
import pytest
from mcp import Client
from mcp.server import MCPServer
from mcp.types import CallToolResult, TextContent
from gewu.domain.contracts import SourceRevisionRecord
from gewu.transports.stdio_connector import (
SelectedFile,
UploadStateStore,
_local_source_id,
_local_source_revision_id,
_read_selected_file,
_remote_factory,
_selected_media_type,
create_connector_server,
upload_selected_file,
)
def test_connector_uploads_allowlisted_bytes_without_exposing_them_to_local_tools(
tmp_path: Path,
) -> None:
async def exercise() -> None:
source = tmp_path / "selected.md"
payload = b"# Private source\n" + b"Exact source bytes stay in the connector.\n" * 20_000
source.write_bytes(payload)
backend = MCPServer("test GEWU backend")
chunks: list[bytes] = []
interrupted = False
extraction_interrupted = False
expected_digest = hashlib.sha256(payload).hexdigest()
@backend.tool(name="source_upload_begin", structured_output=False)
def begin(
project_id: str,
filename: str,
media_type: str,
byte_length: int,
expected_sha256: str,
) -> str:
assert (project_id, filename, media_type, byte_length, expected_sha256) == (
"dome-performance",
"selected.md",
"text/markdown",
len(payload),
expected_digest,
)
return json.dumps({"upload_id": "opaque-upload-id"})
@backend.tool(name="source_upload_chunk", structured_output=False)
def chunk(upload_id: str, offset: int, content_base64: str, chunk_sha256: str) -> str:
nonlocal interrupted
decoded = base64.b64decode(content_base64, validate=True)
assert upload_id == "opaque-upload-id"
assert hashlib.sha256(decoded).hexdigest() == chunk_sha256
assert offset == sum(map(len, chunks))
if offset > 0 and not interrupted:
interrupted = True
raise ValueError("simulated interrupted transfer")
chunks.append(decoded)
return json.dumps({"upload_id": upload_id})
@backend.tool(name="source_upload_status", structured_output=False)
def status(upload_id: str) -> str:
assert upload_id == "opaque-upload-id"
return json.dumps({"verified_offsets": [0] if chunks else []})
@backend.tool(name="source_upload_finalize", structured_output=False)
def finalize(upload_id: str) -> str:
assert upload_id == "opaque-upload-id"
return json.dumps(
{"sha256": hashlib.sha256(b"".join(chunks)).hexdigest(), "status": "staged"}
)
@backend.tool(name="source_extract", structured_output=False)
def extract(project_id: str, artifact_sha256: str, source_revision_id: str) -> str:
nonlocal extraction_interrupted
assert project_id == "dome-performance"
assert artifact_sha256 == expected_digest
assert source_revision_id == "source-r1"
if not extraction_interrupted:
extraction_interrupted = True
raise ValueError("simulated extraction interruption")
return json.dumps({"extraction_id": "e" * 64, "blocks": []})
@asynccontextmanager
async def remote_factory() -> AsyncIterator[Client]:
async with Client(backend) as remote:
yield remote
state_file = tmp_path / "connector-state.json"
selected_files = (SelectedFile("research-note", source),)
async def connector_call() -> CallToolResult:
connector_after_restart = create_connector_server(
selected_files, remote_factory, UploadStateStore(state_file)
)
async with Client(connector_after_restart) as local_after_restart:
return await local_after_restart.call_tool(
"upload_selected_file",
{
"file_id": "research-note",
"project_id": "dome-performance",
"source_revision_id": "source-r1",
},
)
connector = create_connector_server(
selected_files, remote_factory, UploadStateStore(state_file)
)
async with Client(connector) as local:
tools = await local.list_tools()
assert {tool.name for tool in tools.tools} == {
"list_selected_files",
"upload_selected_file",
}
listed = await local.call_tool("list_selected_files", {})
assert not listed.is_error
assert isinstance(listed.content[0], TextContent)
assert "research-note" in listed.content[0].text
assert str(source) not in listed.content[0].text
denied = await local.call_tool(
"upload_selected_file",
{
"file_id": "arbitrary-path",
"project_id": "dome-performance",
"source_revision_id": "source-r1",
},
)
assert denied.is_error
interrupted_result = await connector_call()
assert interrupted_result.is_error
assert state_file.exists()
finalize_interrupted_result = await connector_call()
assert finalize_interrupted_result.is_error
assert json.loads(state_file.read_text())
restarted_result = await connector_call()
assert not restarted_result.is_error
assert isinstance(restarted_result.content[0], TextContent)
receipt = json.loads(restarted_result.content[0].text)
assert receipt["sha256"] == expected_digest
assert receipt["byte_length"] == len(payload)
assert payload[:80].decode() not in restarted_result.content[0].text
source_candidate = SourceRevisionRecord.model_validate_json(
json.dumps(receipt["source_candidate"]), strict=True
)
assert source_candidate.archive_status == "archived"
assert source_candidate.capture_method == "local-file"
assert source_candidate.source_id.startswith("local:")
assert source_candidate.revision_id == "source-r1"
assert source_candidate.artifact_sha256 == expected_digest
assert str(source) not in restarted_result.content[0].text
assert b"".join(chunks) == payload
state = json.loads(state_file.read_text())
assert len(state) == 1
assert "upload_id" not in next(iter(state.values()))
duplicate_result = await connector_call()
assert not duplicate_result.is_error
assert json.loads(duplicate_result.content[0].text) == receipt
asyncio.run(exercise())
def test_connector_rejects_symlinked_selected_paths(tmp_path: Path) -> None:
outside = tmp_path / "outside.md"
outside.write_text("private")
link = tmp_path / "selected.md"
link.symlink_to(outside)
with pytest.raises(ValueError, match="symbolic links"):
_read_selected_file(SelectedFile("selected", link))
def test_local_file_identity_is_path_free_and_revisions_follow_content() -> None:
from datetime import UTC, datetime
captured_at = datetime(2026, 10, 13, tzinfo=UTC)
source_id = _local_source_id("research-note")
first = _local_source_revision_id(source_id, hashlib.sha256(b"first").hexdigest(), captured_at)
second = _local_source_revision_id(
source_id, hashlib.sha256(b"second").hexdigest(), captured_at
)
assert source_id.startswith("local:")
assert "/" not in source_id
assert first != second
assert first.startswith(f"{source_id}:r:")
assert len(first) <= 128
def test_selected_file_media_types_are_explicit() -> None:
assert _selected_media_type(Path("source.pdf")) == "application/pdf"
assert _selected_media_type(Path("source.md")) == "text/markdown"
assert _selected_media_type(Path("source.html")) == "text/html"
assert _selected_media_type(Path("source.txt")) == "text/plain"
assert _selected_media_type(Path("source.vtt")) == "text/vtt"
assert _selected_media_type(Path("source.srt")) == "application/x-subrip"
with pytest.raises(ValueError, match="not supported"):
_selected_media_type(Path("source.exe"))
def test_vtt_connector_candidate_is_marked_as_transcript(tmp_path: Path) -> None:
async def exercise() -> None:
source = tmp_path / "interview.vtt"
content = b"WEBVTT\n\n00:00:01.000 --> 00:00:02.000\nExact words.\n"
source.write_bytes(content)
digest = hashlib.sha256(content).hexdigest()
class Remote:
async def call_tool(self, name: str, arguments: dict[str, object]) -> object:
response: dict[str, object]
if name == "source_upload_begin":
response = {"upload_id": "transcript-upload"}
elif name == "source_upload_chunk":
response = {"upload_id": "transcript-upload"}
elif name == "source_upload_finalize":
response = {"sha256": digest}
elif name == "source_extract":
response = {"status": "complete", "extraction_id": "e" * 64, "blocks": []}
else:
raise AssertionError(f"Unexpected remote tool: {name}")
return type(
"ToolResult",
(),
{
"is_error": False,
"content": [TextContent(type="text", text=json.dumps(response))],
},
)()
receipt = await upload_selected_file(
Remote(), SelectedFile("interview", source), project_id="dome-performance"
)
candidate = SourceRevisionRecord.model_validate_json(
json.dumps(receipt["source_candidate"]), strict=True
)
assert candidate.capture_method == "transcript"
asyncio.run(exercise())
def test_local_connector_returns_archived_candidate_when_pdf_extraction_is_unavailable(
tmp_path: Path,
) -> None:
async def exercise() -> None:
source = tmp_path / "scanned.pdf"
content = b"%PDF-1.4 fixture"
source.write_bytes(content)
digest = hashlib.sha256(content).hexdigest()
calls: list[str] = []
class Remote:
async def call_tool(self, name: str, arguments: dict[str, object]) -> object:
calls.append(name)
response: dict[str, object]
if name == "source_upload_begin":
response = {"upload_id": "active-upload"}
elif name == "source_upload_chunk":
assert arguments["chunk_sha256"] == digest
response = {"upload_id": "active-upload"}
elif name == "source_upload_finalize":
response = {"sha256": digest}
elif name == "source_extract":
response = {
"status": "unavailable",
"error_code": "PDF_NO_TEXT",
"blocks": [],
}
else:
raise AssertionError(f"Unexpected remote tool: {name}")
return type(
"ToolResult",
(),
{
"is_error": False,
"content": [TextContent(type="text", text=json.dumps(response))],
},
)()
state = UploadStateStore(tmp_path / "state.json")
first = await upload_selected_file(
Remote(),
SelectedFile("scanned-paper", source),
project_id="dome-performance",
state_store=state,
)
second = await upload_selected_file(
Remote(),
SelectedFile("scanned-paper", source),
project_id="dome-performance",
state_store=state,
)
candidate = SourceRevisionRecord.model_validate_json(
json.dumps(first["source_candidate"]), strict=True
)
assert candidate.archive_status == "archived"
assert candidate.artifact_sha256 == digest
assert len(candidate.revision_id) <= 128
assert first["extraction_status"] == "unavailable"
assert first["extraction_error_code"] == "PDF_NO_TEXT"
assert second["source_candidate"] == first["source_candidate"]
assert calls.count("source_upload_begin") == 1
assert calls.count("source_extract") == 2
state_record = next(iter(json.loads((tmp_path / "state.json").read_text()).values()))
assert "completed_receipt" not in state_record
assert str(source) not in json.dumps(first)
asyncio.run(exercise())
def test_connector_rejects_cleartext_remote_service_urls() -> None:
with pytest.raises(ValueError, match="HTTPS"):
_remote_factory("http://gewu.example/mcp", "gwu_test")
_remote_factory("https://gewu.example/mcp", "gwu_test")
_remote_factory("http://localhost:8000/mcp", "gwu_test")
def test_remote_factory_verifies_service_certificate_before_authentication(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from contextlib import asynccontextmanager
import gewu.transports.stdio_connector as connector_module
observed: dict[str, object] = {}
class HttpClient:
def __init__(self, **kwargs: object) -> None:
observed.update(kwargs)
async def __aenter__(self) -> object:
return self
async def __aexit__(self, *args: object) -> None:
return None
class RemoteClient:
def __init__(self, transport: object) -> None:
self.transport = transport
async def __aenter__(self) -> object:
await self.transport.__aenter__()
return self
async def __aexit__(self, *args: object) -> None:
await self.transport.__aexit__(*args)
return None
@asynccontextmanager
async def transport_factory(url: str, *, http_client: object):
observed["url"] = url
observed["http_client"] = http_client
yield object()
monkeypatch.setattr(connector_module.httpx2, "AsyncClient", HttpClient)
monkeypatch.setattr(connector_module, "Client", RemoteClient)
monkeypatch.setattr(connector_module, "streamable_http_client", transport_factory)
async def exercise() -> None:
async with _remote_factory("https://gewu.example/mcp", "secret-token")() as remote:
assert isinstance(remote, RemoteClient)
asyncio.run(exercise())
assert observed["verify"] is True
assert observed["headers"] == {"Authorization": "Bearer secret-token"}
assert observed["url"] == "https://gewu.example/mcp"
assert "secret-token" not in str(observed["url"])