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

+243
View File
@@ -0,0 +1,243 @@
"""Measure the SQLite search index on a deterministic synthetic corpus.
This is an adapter benchmark, not the PRD's end-to-end or semantic-quality gate.
Run with ``./scripts/python-tool python scripts/benchmark_search.py``.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import math
import os
import platform
import sqlite3
import statistics
import tempfile
import time
from collections.abc import Sequence
from datetime import UTC, datetime
from pathlib import Path
from gewu.adapters.sqlite_search import SQLiteSearchIndex
from gewu.domain.contracts import (
Claim,
Confidence,
EvidenceReference,
KnowledgeNode,
Lifecycle,
Locator,
NodeType,
ReviewState,
SupportAssessment,
SupportStatus,
)
from gewu.domain.validation import assertion_digest, evidence_digest
def base_node() -> KnowledgeNode:
text = "Interactive experiences welcomed visitors during the synthetic benchmark."
assertion = assertion_digest(text)
evidence = EvidenceReference(
source_revision_id="benchmark:source:r1",
extraction_id="a" * 64,
assertion_path="claims[0].text",
assertion_digest=assertion,
quote=text,
locator=Locator(kind="text_span", block_id="benchmark-block", start=0, end=len(text)),
normalization_profile="unicode-nfc-lf-v1",
)
now = datetime.now(UTC)
claim = Claim(
claim_id="benchmark:claim",
text=text,
evidence_type="reported",
evidence=(evidence,),
assessment=SupportAssessment(
assessment_id="benchmark:assessment",
assertion_digest=assertion,
evidence_digests=(evidence_digest(evidence),),
assessor_id="benchmark:assessor",
run_id="benchmark:run",
method="synthetic benchmark fixture",
status=SupportStatus.SUPPORTED,
rationale="Fixture only; not a semantic assessment.",
),
)
return KnowledgeNode(
node_id="benchmark:template",
project_id="bench-project-000",
node_type=NodeType.FINDING,
title="Interactive visitor evidence template",
summary=text,
created=now,
updated=now,
review_state=ReviewState.REVIEWED,
lifecycle=Lifecycle.ACTIVE,
confidence=Confidence.MEDIUM,
claim_support="supported",
run_id="benchmark:run",
agent_id="benchmark:agent",
model_id="synthetic-fixture",
claims=(claim,),
)
class SyntheticRepository:
"""Small in-memory canonical port that feeds validated production records."""
def __init__(self, nodes_per_project: int, projects: int) -> None:
revision = hashlib.sha256(f"benchmark:{nodes_per_project}:{projects}".encode()).hexdigest()
self._revision = revision
self._records: dict[str, bytes] = {}
self.project_ids = tuple(f"bench-project-{index:03d}" for index in range(projects))
template = base_node()
for project_index, project_id in enumerate(self.project_ids):
project_key = hashlib.sha256(project_id.encode()).hexdigest()
for node_index in range(nodes_per_project):
node_id = f"benchmark:{project_index:03d}:node-{node_index:06d}"
node_key = hashlib.sha256(node_id.encode()).hexdigest()
path = f"projects/{project_key}/nodes/{node_key}.yaml"
node = template.model_copy(
update={
"node_id": node_id,
"project_id": project_id,
"title": f"Interactive visitor evidence {project_index} {node_index}",
"summary": (
"Interactive experiences welcomed visitors; "
f"synthetic record {node_index}."
),
}
)
self._records[path] = node.model_dump_json().encode("utf-8")
def head(self) -> str:
return self._revision
def list_files(self, revision: str, prefix: str) -> tuple[str, ...]:
if revision != self._revision:
raise ValueError("Unknown synthetic revision")
return tuple(path for path in self._records if path.startswith(prefix))
def read_file(self, revision: str, path: str) -> bytes | None:
if revision != self._revision:
raise ValueError("Unknown synthetic revision")
return self._records.get(path)
def percentile(samples: Sequence[float], value: float) -> float:
ordered = sorted(samples)
position = max(0, math.ceil(len(ordered) * value) - 1)
return ordered[position]
def processor_name() -> str:
reported = platform.processor()
if reported:
return reported
if platform.system() == "Linux":
try:
for line in Path("/proc/cpuinfo").read_text(encoding="utf-8").splitlines():
if line.startswith("model name"):
return line.partition(":")[2].strip()
except OSError:
pass
return "not reported"
def run(nodes_per_project: int, projects: int, samples: int, root: Path) -> dict[str, object]:
build_started = time.perf_counter()
repository = SyntheticRepository(nodes_per_project, projects)
seed_seconds = time.perf_counter() - build_started
index = SQLiteSearchIndex(root / "search.sqlite3", repository) # type: ignore[arg-type]
cold_started = time.perf_counter()
cold_result = index.search(
query="interactive visitors",
project_ids=(repository.project_ids[0],),
include_shared=False,
limit=20,
)
cold_seconds = time.perf_counter() - cold_started
timings: list[float] = []
for _ in range(samples):
started = time.perf_counter()
result = index.search(
query="interactive visitors",
project_ids=(repository.project_ids[0],),
include_shared=False,
limit=20,
)
timings.append((time.perf_counter() - started) * 1000)
if len(result.nodes) != 20:
raise RuntimeError(f"Expected 20 results, received {len(result.nodes)}")
total_nodes = nodes_per_project * projects
return {
"benchmark": "sqlite-search-adapter-synthetic-v1",
"evidence_scope": (
"synthetic adapter benchmark; excludes HTTP/auth, Git object reads, "
"and semantic relevance"
),
"environment": {
"platform": platform.platform(),
"processor": processor_name(),
"logical_cpus": os.cpu_count(),
"python": platform.python_version(),
"sqlite": sqlite3.sqlite_version,
},
"workload": {
"projects": projects,
"nodes_per_project": nodes_per_project,
"total_nodes": total_nodes,
"query_project": repository.project_ids[0],
"query": "interactive visitors",
"page_size": 20,
"warm_samples": samples,
},
"measurements": {
"synthetic_seed_seconds": round(seed_seconds, 3),
"cold_index_rebuild_and_first_search_seconds": round(cold_seconds, 3),
"warm_search_ms": {
"p50": round(statistics.median(timings), 3),
"p95": round(percentile(timings, 0.95), 3),
"max": round(max(timings), 3),
},
"first_result_count": len(cold_result.nodes),
},
"limitations": [
"synthetic records reuse one valid claim/citation shape and are not a relevance corpus",
"in-memory canonical reads omit Git filesystem and HTTP/API overhead",
"the PRD hardware, cache state, concurrency, and percentile decisions are not fixed",
],
}
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--nodes-per-project", type=int, default=10_000)
parser.add_argument("--projects", type=int, default=1)
parser.add_argument("--samples", type=int, default=50)
parser.add_argument("--output", type=Path)
args = parser.parse_args()
if min(args.nodes_per_project, args.projects, args.samples) < 1:
parser.error("node, project, and sample counts must be positive")
with tempfile.TemporaryDirectory(prefix="gewu-search-benchmark-") as temporary:
report = run(
args.nodes_per_project,
args.projects,
args.samples,
Path(temporary),
)
rendered = json.dumps(report, indent=2, sort_keys=True) + "\n"
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(rendered, encoding="utf-8")
print(rendered, end="")
if __name__ == "__main__":
main()
+30
View File
@@ -0,0 +1,30 @@
"""Enforce inward dependencies in the domain package."""
import ast
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
FORBIDDEN = ("fastapi", "mcp", "gewu.adapters", "gewu.bootstrap", "gewu.transports")
def main() -> None:
domain = ROOT / "src" / "gewu" / "domain"
violations: list[str] = []
for path in domain.rglob("*.py"):
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
for node in ast.walk(tree):
if isinstance(node, ast.Import):
names = [alias.name for alias in node.names]
elif isinstance(node, ast.ImportFrom):
names = [node.module or ""]
else:
continue
for name in names:
if any(name == prefix or name.startswith(prefix + ".") for prefix in FORBIDDEN):
violations.append(f"{path.relative_to(ROOT)}:{node.lineno}: {name}")
if violations:
raise SystemExit("Forbidden domain dependencies:\n" + "\n".join(violations))
if __name__ == "__main__":
main()
+116
View File
@@ -0,0 +1,116 @@
"""Bind the GEWU agent pack to the MCP tools actually exposed by the service."""
from __future__ import annotations
import asyncio
import hashlib
import json
import shutil
import tempfile
from pathlib import Path
from typing import Any
from gewu.adapters.git_repository import GitCanonicalRepository
from gewu.adapters.sqlite_identity import SQLiteIdentityStore
from gewu.adapters.sqlite_search import SQLiteSearchIndex
from gewu.adapters.text_extractor import TextMarkdownExtractor
from gewu.application.acceptance import AcceptanceService
from gewu.application.context import ValidationContext
from gewu.application.reading import CanonicalNodeReader, CanonicalSourceReader
from gewu.application.search import CanonicalSearchService
from gewu.application.uploads import LocalArtifactStore
from gewu.domain.operations import Transport, operations_for
from gewu.transports.mcp_server import create_mcp_server
ROOT = Path(__file__).resolve().parents[1]
PACK = ROOT / "agents" / "gewu"
CONTRACT_PATH = PACK / "contracts" / "service-v1.json"
SCHEMA_OUTPUT = PACK / "contracts" / "schemas"
CATALOG_OUTPUT = PACK / "contracts" / "catalog-v1.json"
def _sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
async def _tool_inventory(data_root: Path) -> list[dict[str, Any]]:
repository = GitCanonicalRepository(data_root / "knowledge.git")
identities = SQLiteIdentityStore(data_root / "identity.sqlite3")
artifact_store = LocalArtifactStore(data_root / "artifacts")
def context_provider() -> ValidationContext:
return ValidationContext({}, {}, {}, {}, {})
acceptance_service = AcceptanceService(
repository=repository,
artifact_store=artifact_store,
workflow_root=data_root / "workflow",
context_provider=context_provider,
actor_is_current=lambda _actor: True,
)
search_service = CanonicalSearchService(
index=SQLiteSearchIndex(data_root / "index.sqlite3", repository),
project_ids_provider=lambda: (),
canonical_head=repository.head,
)
server = create_mcp_server(
identity_provider=identities,
node_reader=CanonicalNodeReader(repository),
source_reader=CanonicalSourceReader(repository),
search_service=search_service,
artifact_store=artifact_store,
extractor=TextMarkdownExtractor(),
acceptance_service=acceptance_service,
context_provider=context_provider,
)
tools = await server.list_tools()
return [
{
"name": tool.name,
"description": tool.description,
"input_schema": tool.input_schema,
}
for tool in tools
]
def build_contract(data_root: Path, *, schema_output: Path | None = None) -> dict[str, Any]:
catalog_path = ROOT / "schema" / "v1" / "catalog.json"
catalog = json.loads(catalog_path.read_text(encoding="utf-8"))
tools = asyncio.run(_tool_inventory(data_root))
expected_names = {item.name for item in operations_for(Transport.MCP)}
actual_names = {tool["name"] for tool in tools}
if actual_names != expected_names:
raise RuntimeError("MCP tool inventory does not match the explicit operation catalog")
if schema_output is not None:
schema_output.mkdir(parents=True, exist_ok=True)
record_schemas = []
for source in sorted((ROOT / "schema" / "v1" / "records").glob("*.schema.json")):
destination = schema_output / source.name if schema_output is not None else source
if schema_output is not None:
shutil.copyfile(source, destination)
record_schemas.append({"path": f"schemas/{source.name}", "sha256": _sha256(destination)})
return {
"contract_id": "gewu.service.v1",
"contract_version": 1,
"api_version": catalog["api_version"],
"catalog_sha256": _sha256(catalog_path),
"operations": [item for item in catalog["operations"] if "mcp" in item["transports"]],
"record_schemas": record_schemas,
"tools": tools,
}
def main() -> None:
CONTRACT_PATH.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(ROOT / "schema" / "v1" / "catalog.json", CATALOG_OUTPUT)
with tempfile.TemporaryDirectory(prefix="gewu-agent-contract-") as temporary:
contract = build_contract(Path(temporary), schema_output=SCHEMA_OUTPUT)
CONTRACT_PATH.write_text(
json.dumps(contract, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
if __name__ == "__main__":
main()
+16
View File
@@ -0,0 +1,16 @@
"""Export the application operation allowlist for generated adapters."""
from pathlib import Path
from gewu.domain.operations import catalog_bytes
ROOT = Path(__file__).resolve().parents[1]
DESTINATION = ROOT / "schema" / "v1" / "catalog.json"
def main() -> None:
DESTINATION.write_bytes(catalog_bytes())
if __name__ == "__main__":
main()
+64
View File
@@ -0,0 +1,64 @@
"""Export the composed HTTP API schema without touching the operator's data root."""
from __future__ import annotations
import json
import os
import tempfile
from pathlib import Path
from typing import Any
from gewu.bootstrap.app import create_local_app
ROOT = Path(__file__).resolve().parents[1]
DESTINATION = ROOT / "schema" / "v1" / "openapi.json"
def componentize_record_schema(
name: str, record_schema: dict[str, Any], components: dict[str, Any]
) -> None:
definitions = record_schema.pop("$defs", {})
def rewrite_local_refs(value: Any) -> Any:
if isinstance(value, list):
return [rewrite_local_refs(item) for item in value]
if not isinstance(value, dict):
return value
rewritten = {key: rewrite_local_refs(item) for key, item in value.items() if key != "$defs"}
reference = rewritten.get("$ref")
if isinstance(reference, str) and reference.startswith("#/$defs/"):
definition_name = reference.removeprefix("#/$defs/")
rewritten["$ref"] = f"#/components/schemas/{name}__{definition_name}"
return rewritten
for definition_name, definition in definitions.items():
component_name = f"{name}__{definition_name}"
components.setdefault(component_name, rewrite_local_refs(definition))
components.setdefault(name, rewrite_local_refs(record_schema))
def main() -> None:
previous_data_root = os.environ.get("GEWU_DATA_ROOT")
with tempfile.TemporaryDirectory(prefix="gewu-openapi-") as temporary:
os.environ["GEWU_DATA_ROOT"] = str(Path(temporary) / "data")
try:
schema: dict[str, Any] = create_local_app().openapi()
finally:
if previous_data_root is None:
os.environ.pop("GEWU_DATA_ROOT", None)
else:
os.environ["GEWU_DATA_ROOT"] = previous_data_root
schema_components = schema.setdefault("components", {}).setdefault("schemas", {})
for record_schema in sorted((ROOT / "schema" / "v1" / "records").glob("*.schema.json")):
component_name = record_schema.name.removesuffix(".schema.json")
schema_value = json.loads(record_schema.read_text(encoding="utf-8"))
componentize_record_schema(component_name, schema_value, schema_components)
DESTINATION.parent.mkdir(parents=True, exist_ok=True)
DESTINATION.write_text(
json.dumps(schema, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
if __name__ == "__main__":
main()
+64
View File
@@ -0,0 +1,64 @@
"""Export public Pydantic records as versioned JSON Schemas."""
import json
from pathlib import Path
from gewu.domain.contracts import (
AcceptanceManifest,
AutoAcceptPolicy,
DomainError,
ExtractionRecord,
KnowledgeNode,
NeighborRequest,
ProjectRecord,
ProposalReceipt,
ProposalRevision,
ProposalStatus,
ReadEnvelope,
RunCheckpointRequest,
RunCreateRequest,
RunRecord,
Scope,
SearchRequest,
ServiceDiagnostics,
SourceRevisionRecord,
TypedEdge,
)
ROOT = Path(__file__).resolve().parents[1]
DESTINATION = ROOT / "schema" / "v1" / "records"
MODELS = (
AcceptanceManifest,
AutoAcceptPolicy,
DomainError,
ExtractionRecord,
KnowledgeNode,
NeighborRequest,
ProjectRecord,
ProposalReceipt,
ProposalRevision,
ProposalStatus,
ReadEnvelope,
ServiceDiagnostics,
RunCheckpointRequest,
RunCreateRequest,
RunRecord,
SearchRequest,
Scope,
SourceRevisionRecord,
TypedEdge,
)
def main() -> None:
DESTINATION.mkdir(parents=True, exist_ok=True)
for model in MODELS:
output = DESTINATION / f"{model.__name__}.schema.json"
output.write_text(
json.dumps(model.model_json_schema(mode="validation"), indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
if __name__ == "__main__":
main()
+34
View File
@@ -0,0 +1,34 @@
"""Provision one local service credential; the bearer key is shown only once."""
from __future__ import annotations
import argparse
import os
from pathlib import Path
from gewu.adapters.sqlite_identity import SQLiteIdentityStore
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--credential-id", required=True)
parser.add_argument("--principal-id", required=True)
parser.add_argument(
"--role", action="append", choices=("admin", "editor", "viewer", "agent"), required=True
)
parser.add_argument("--project", action="append", default=[])
arguments = parser.parse_args()
data_root = Path(os.environ.get("GEWU_DATA_ROOT", "./.data/gewu")).expanduser().absolute()
data_root.mkdir(parents=True, exist_ok=True, mode=0o700)
token = SQLiteIdentityStore(data_root / "identity.sqlite3").provision(
credential_id=arguments.credential_id,
principal_id=arguments.principal_id,
roles=tuple(arguments.role),
project_ids=tuple(arguments.project),
)
print("Store this bearer credential securely; it cannot be displayed again:")
print(token)
if __name__ == "__main__":
main()
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail
if command -v uv >/dev/null 2>&1; then
exec uv run "$@"
fi
python_bin=".venv/bin/python"
if [[ ! -x "$python_bin" ]]; then
python3 -m venv .venv
fi
if ! "$python_bin" -m pip --version >/dev/null 2>&1; then
"$python_bin" -m ensurepip --upgrade
fi
marker=".venv/.gewu-dev-installed"
if [[ ! -f "$marker" ]]; then
"$python_bin" -m pip install --disable-pip-version-check -e '.[dev]'
touch "$marker"
fi
tool="$1"
shift
exec ".venv/bin/$tool" "$@"
+74
View File
@@ -0,0 +1,74 @@
"""Install the explicitly labeled two-project test registry for disposable work."""
from __future__ import annotations
import argparse
import json
import os
import secrets
from pathlib import Path
from gewu.application.paths import reject_symlink_components
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--replace", action="store_true")
arguments = parser.parse_args()
data_root = Path(os.environ.get("GEWU_DATA_ROOT", "./.data/gewu")).expanduser().absolute()
reject_symlink_components(data_root)
control_root = data_root / "control"
reject_symlink_components(control_root)
control_root.mkdir(parents=True, exist_ok=True, mode=0o700)
control_root.chmod(0o700)
destination = control_root / "registry.json"
if destination.exists() and not arguments.replace:
raise SystemExit("Registry exists; pass --replace only to deliberately reset it")
fixture_path = (
Path(__file__).resolve().parents[1] / "schema" / "fixtures" / "pilot-projects.json"
)
def reject_duplicate_keys(pairs: list[tuple[str, object]]) -> dict[str, object]:
result: dict[str, object] = {}
for key, value in pairs:
if key in result:
raise ValueError("Test fixture has duplicate JSON keys")
result[key] = value
return result
fixture = json.loads(
fixture_path.read_text(encoding="utf-8"), object_pairs_hook=reject_duplicate_keys
)
project_fields = {
"project_id",
"title",
"status",
"north_star",
"key_questions",
"autonomy_policy_id",
}
projects = []
for item in fixture["projects"]:
project = {key: value for key, value in item.items() if key in project_fields}
projects.append(project)
config = {
"schema_version": 1,
"projects": projects,
"policies": fixture["test_policies"],
}
temporary = control_root / f".{secrets.token_hex(12)}.tmp"
try:
with temporary.open("x", encoding="utf-8") as stream:
json.dump(config, stream, ensure_ascii=False, allow_nan=False, indent=2)
stream.flush()
os.fsync(stream.fileno())
os.chmod(temporary, 0o600)
os.replace(temporary, destination)
finally:
temporary.unlink(missing_ok=True)
print("Installed disposable fixture projects and test-only acceptance policies.")
if __name__ == "__main__":
main()