427 lines
18 KiB
Python
427 lines
18 KiB
Python
"""Deterministic in-memory ContextForge-compatible target."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from copy import deepcopy
|
|
from typing import Any
|
|
from uuid import UUID, uuid4
|
|
|
|
import httpx
|
|
|
|
|
|
class FakeContextForge:
|
|
"""Fake REST gateway for offline adapter and reconciliation tests."""
|
|
|
|
def __init__(self) -> None:
|
|
self.gateways: dict[str, dict[str, Any]] = {}
|
|
self.tools: dict[str, list[dict[str, Any]]] = {}
|
|
self.prompts: dict[str, list[dict[str, Any]]] = {}
|
|
self.servers: dict[str, dict[str, Any]] = {}
|
|
self.requests: list[tuple[str, str, dict[str, str]]] = []
|
|
self.fail_next: int | None = None
|
|
|
|
_gateway_tools = {
|
|
"itworx-stitch": (
|
|
"stitch.project.list",
|
|
"stitch.project.get",
|
|
"stitch.project.create",
|
|
"stitch.screen.list",
|
|
"stitch.screen.get",
|
|
"stitch.screen.generate",
|
|
"stitch.screen.edit",
|
|
"stitch.screen.variants",
|
|
),
|
|
"itworx-knowledge": (
|
|
"knowledge.document.search",
|
|
"knowledge.document.get",
|
|
"knowledge.source.list",
|
|
"knowledge.index.status",
|
|
"knowledge.adr.list",
|
|
"knowledge.project.summary",
|
|
),
|
|
"itworx-gitea": (
|
|
"gitea.capabilities.get",
|
|
"gitea.connection.check",
|
|
"gitea.selftest.run",
|
|
"gitea.repository.list",
|
|
"gitea.repository.get",
|
|
"gitea.repository.context",
|
|
"gitea.repository.health",
|
|
"gitea.branchprotection.get",
|
|
"gitea.branchprotection.preview",
|
|
"gitea.branch.list",
|
|
"gitea.commit.list",
|
|
"gitea.commit.get",
|
|
"gitea.ref.compare",
|
|
"gitea.release.list",
|
|
"gitea.issue.list",
|
|
"gitea.issue.search",
|
|
"gitea.code.search",
|
|
"search",
|
|
"fetch",
|
|
"gitea.file.read",
|
|
"gitea.tree.list",
|
|
"gitea.changeset.preview",
|
|
"gitea.validation.get",
|
|
"gitea.validation.failure.get",
|
|
"gitea.validation.list",
|
|
"gitea.change.status",
|
|
"gitea.change.review",
|
|
"gitea.change.reconcile",
|
|
"gitea.workspace.status",
|
|
"gitea.portfolio.health",
|
|
"gitea.portfolio.governance.preview",
|
|
"gitea.portfolio.governance.verify",
|
|
"gitea.orchestration.plan",
|
|
"gitea.pullrequest.list",
|
|
"gitea.pullrequest.get",
|
|
"gitea.pullrequest.feedback",
|
|
"gitea.commitstatus.list",
|
|
"gitea.repository.create",
|
|
"gitea.branchprotection.ensure",
|
|
"gitea.portfolio.governance.apply",
|
|
"gitea.branch.create",
|
|
"gitea.issue.create",
|
|
"gitea.issue.update",
|
|
"gitea.issue.comment.create",
|
|
"gitea.file.create",
|
|
"gitea.file.update",
|
|
"gitea.file.delete",
|
|
"gitea.pullrequest.create",
|
|
"gitea.pullrequest.update",
|
|
"gitea.pullrequest.close",
|
|
"gitea.pullrequest.validation.ensure",
|
|
"gitea.pullrequest.sync",
|
|
"gitea.pullrequest.merge",
|
|
"gitea.branch.delete",
|
|
"gitea.changeset.apply",
|
|
"gitea.validation.run",
|
|
"gitea.workspace.run",
|
|
"gitea.orchestration.start",
|
|
),
|
|
"itworx-unraid": (
|
|
"unraid.host.status",
|
|
"unraid.storage.list",
|
|
"unraid.network.list",
|
|
"unraid.container.list",
|
|
"unraid.container.inspect",
|
|
"unraid.container.health",
|
|
"unraid.container.logs",
|
|
"unraid.project.summary",
|
|
),
|
|
"itworx-mobilityops": (
|
|
"mobilityops.operations.summary",
|
|
"mobilityops.attention.list",
|
|
"mobilityops.vehicle.get",
|
|
"mobilityops.knowledge.search",
|
|
),
|
|
"itworx-projectbrain": (
|
|
"projectbrain.project.list",
|
|
"projectbrain.repository.list",
|
|
"projectbrain.portfolio.get",
|
|
"projectbrain.portfolio.repository.list",
|
|
"projectbrain.portfolio.repository.reconcile",
|
|
"projectbrain.portfolio.repository.exclusion.set",
|
|
"projectbrain.repository.search",
|
|
"projectbrain.repository.fetch",
|
|
"projectbrain.context.get",
|
|
"projectbrain.workspace.render",
|
|
"projectbrain.modules.get",
|
|
"projectbrain.brainstorm.save",
|
|
"projectbrain.knowledge.save",
|
|
"projectbrain.documentation.save",
|
|
"projectbrain.memory.save",
|
|
"projectbrain.contextpack.create",
|
|
"projectbrain.feedback.record",
|
|
"projectbrain.delivery.list",
|
|
"projectbrain.delivery.get",
|
|
"projectbrain.delivery.create",
|
|
"projectbrain.delivery.update",
|
|
"projectbrain.repository.tree",
|
|
"projectbrain.repository.file",
|
|
"projectbrain.repository.branches",
|
|
"projectbrain.repository.pullrequests",
|
|
"projectbrain.delivery.start",
|
|
"projectbrain.delivery.file.write",
|
|
"projectbrain.delivery.file.delete",
|
|
"projectbrain.delivery.changeset.apply",
|
|
"projectbrain.delivery.validation.record",
|
|
"projectbrain.delivery.validation.runs",
|
|
"projectbrain.delivery.validation.sync",
|
|
"projectbrain.delivery.complete",
|
|
"projectbrain.delivery.merge",
|
|
"projectbrain.analysis.start",
|
|
"projectbrain.analysis.list",
|
|
"projectbrain.analysis.status",
|
|
"projectbrain.analysis.results",
|
|
"projectbrain.analysis.cancel",
|
|
"projectbrain.analysis.render",
|
|
"projectbrain.finding.list",
|
|
"projectbrain.finding.get",
|
|
"projectbrain.finding.issue.sync",
|
|
"projectbrain.finding.investigation.start",
|
|
"projectbrain.finding.investigation.get",
|
|
"projectbrain.finding.investigation.save",
|
|
"projectbrain.finding.suppress",
|
|
"projectbrain.finding.delivery.create",
|
|
"projectbrain.finding.verify",
|
|
"projectbrain.autonomy.projects.list",
|
|
"projectbrain.autonomy.executor.heartbeat",
|
|
"projectbrain.autonomy.state.get",
|
|
"projectbrain.autonomy.configure",
|
|
"projectbrain.autonomy.roadmap.import",
|
|
"projectbrain.autonomy.resume",
|
|
"projectbrain.autonomy.work.claim",
|
|
"projectbrain.autonomy.work.report",
|
|
),
|
|
"itworx-appops": (
|
|
"server.summary.get",
|
|
"server.application.list",
|
|
"server.container.list",
|
|
"server.safety.findings",
|
|
"server.action.list",
|
|
"server.operation.preview",
|
|
"server.audit.search",
|
|
"server.knowledge.query",
|
|
"server.operation.request",
|
|
"server.operation.status",
|
|
"server.operation.execute",
|
|
"server.job.status",
|
|
"server.job.cancel",
|
|
"server.ci.runners.status",
|
|
"server.project.list",
|
|
"server.project.reconcile",
|
|
"server.deployment.preflight",
|
|
"server.deployment.capabilities",
|
|
"server.deployment.enrollment.preflight",
|
|
"server.deployment.target.enroll",
|
|
"server.deployment.enrollment.status",
|
|
"server.deployment.apply",
|
|
"server.deployment.status",
|
|
"server.deployment.verify",
|
|
"server.deployment.rollback",
|
|
),
|
|
"itworx-opencode": (
|
|
"opencode.capabilities.get",
|
|
"opencode.models.list",
|
|
"opencode.workspace.ensure",
|
|
"opencode.job.preview",
|
|
"opencode.job.start",
|
|
"opencode.jobs.list",
|
|
"opencode.job.status",
|
|
"opencode.job.result",
|
|
"opencode.job.cancel",
|
|
"opencode.delivery.control.repositories.list",
|
|
"opencode.delivery.control.plan",
|
|
"opencode.delivery.control.start",
|
|
"opencode.delivery.control.jobs.list",
|
|
"opencode.delivery.control.status",
|
|
"opencode.delivery.control.result",
|
|
"opencode.delivery.control.retry",
|
|
"opencode.delivery.control.cancel",
|
|
),
|
|
"itworx-opencode-readonly": (
|
|
"opencode.analysis.capabilities.get",
|
|
"opencode.analysis.models.list",
|
|
"opencode.analysis.start",
|
|
"opencode.analysis.jobs.list",
|
|
"opencode.analysis.status",
|
|
"opencode.analysis.result",
|
|
"opencode.delivery.repositories.list",
|
|
"opencode.delivery.jobs.list",
|
|
"opencode.delivery.status",
|
|
"opencode.delivery.result",
|
|
),
|
|
}
|
|
|
|
_gateway_prompts = {
|
|
"itworx-knowledge": ("project-context-pack", "pre-change-impact-review"),
|
|
"itworx-gitea": (
|
|
"repository-engineer",
|
|
"repository-change-review",
|
|
"repository-maintenance",
|
|
"portfolio-engineer",
|
|
"release-readiness",
|
|
),
|
|
"itworx-unraid": ("production-incident-triage", "deployment-health-review"),
|
|
"itworx-mobilityops": ("fleet-operations-morning-brief",),
|
|
}
|
|
|
|
@staticmethod
|
|
def _tool_payload(tool_name: str) -> dict[str, Any]:
|
|
payload: dict[str, Any] = {"id": str(uuid4()), "name": tool_name}
|
|
if tool_name == "gitea.repository.create":
|
|
payload["inputSchema"] = {
|
|
"type": "object",
|
|
"properties": {
|
|
field: {"type": field_type}
|
|
for field, field_type in (
|
|
("name", "string"),
|
|
("description", "string"),
|
|
("private", "boolean"),
|
|
("autoInit", "boolean"),
|
|
("defaultBranch", "string"),
|
|
)
|
|
},
|
|
"required": ["name", "description", "private", "autoInit", "defaultBranch"],
|
|
}
|
|
payload["inputSchema"]["properties"]["autoInit"]["const"] = True
|
|
return payload
|
|
|
|
def transport(self) -> httpx.MockTransport:
|
|
return httpx.MockTransport(self._handle)
|
|
|
|
def _response(self, request: httpx.Request, status: int, payload: Any) -> httpx.Response:
|
|
return httpx.Response(status, json=payload, request=request)
|
|
|
|
def _handle(self, request: httpx.Request) -> httpx.Response:
|
|
self.requests.append(
|
|
(
|
|
request.method,
|
|
request.url.path,
|
|
{
|
|
key: "<redacted>" if key.lower() == "authorization" else value
|
|
for key, value in request.headers.items()
|
|
},
|
|
)
|
|
)
|
|
if self.fail_next is not None:
|
|
status = self.fail_next
|
|
self.fail_next = None
|
|
return self._response(request, status, {"error": "simulated"})
|
|
path = request.url.path
|
|
if path.startswith("/v1/"):
|
|
path = path[3:]
|
|
if request.method == "GET" and path == "/health":
|
|
return self._response(request, 200, {"status": "healthy", "version": "1.0.6"})
|
|
if request.method == "GET" and path == "/ready":
|
|
return self._response(request, 200, {"status": "ready"})
|
|
if request.method == "GET" and path == "/version":
|
|
return self._response(request, 200, {"version": "1.0.6"})
|
|
if path == "/gateways":
|
|
if request.method == "GET":
|
|
return self._response(
|
|
request, 200, {"gateways": list(self.gateways.values()), "nextCursor": None}
|
|
)
|
|
if request.method == "POST":
|
|
payload = json.loads(request.content)
|
|
name = str(payload["name"])
|
|
if any(item["name"] == name for item in self.gateways.values()):
|
|
return self._response(request, 409, {"error": "conflict"})
|
|
gateway = {
|
|
"id": f"gateway-{uuid4().hex}",
|
|
"name": name,
|
|
"url": payload["url"],
|
|
"description": payload.get("description", ""),
|
|
"transport": payload.get("transport", "STREAMABLEHTTP"),
|
|
"enabled": payload.get("enabled", True),
|
|
"status": "active",
|
|
"passthrough_headers": payload.get("passthrough_headers", []),
|
|
}
|
|
self.gateways[gateway["id"]] = gateway
|
|
self.tools[gateway["id"]] = [
|
|
self._tool_payload(tool_name) for tool_name in self._gateway_tools.get(name, ())
|
|
]
|
|
self.prompts[gateway["id"]] = [
|
|
{"id": str(uuid4()), "name": prompt_name}
|
|
for prompt_name in self._gateway_prompts.get(name, ())
|
|
]
|
|
return self._response(request, 201, deepcopy(gateway))
|
|
if path == "/tools" and request.method == "GET":
|
|
gateway_id = request.url.params.get("gateway_id", "")
|
|
return self._response(request, 200, deepcopy(self.tools.get(gateway_id, [])))
|
|
if path == "/prompts" and request.method == "GET":
|
|
gateway_id = request.url.params.get("gateway_id", "")
|
|
return self._response(request, 200, deepcopy(self.prompts.get(gateway_id, [])))
|
|
if path == "/servers":
|
|
if request.method == "GET":
|
|
return self._response(request, 200, deepcopy(list(self.servers.values())))
|
|
if request.method == "POST":
|
|
payload = json.loads(request.content)
|
|
payload = payload.get("server", payload)
|
|
external_id = UUID(str(payload["id"])).hex if payload.get("id") else uuid4().hex
|
|
server = {
|
|
"id": external_id,
|
|
"name": payload["name"],
|
|
"associated_tool_ids": payload.get("associated_tools", []),
|
|
"associated_prompt_ids": payload.get("associated_prompts", []),
|
|
"enabled": True,
|
|
"visibility": payload.get("visibility", "private"),
|
|
}
|
|
self.servers[external_id] = server
|
|
return self._response(request, 201, deepcopy(server))
|
|
parts = path.strip("/").split("/")
|
|
if len(parts) == 2 and parts[0] == "tools" and request.method == "PUT":
|
|
payload = json.loads(request.content)
|
|
for gateway_tools in self.tools.values():
|
|
tool = next((item for item in gateway_tools if item.get("id") == parts[1]), None)
|
|
if tool is not None:
|
|
if "customName" in payload:
|
|
tool["customName"] = payload["customName"]
|
|
return self._response(request, 200, deepcopy(tool))
|
|
return self._response(request, 404, {"error": "not_found"})
|
|
if len(parts) >= 2 and parts[0] == "gateways":
|
|
stored = self.gateways.get(parts[1])
|
|
if stored is None:
|
|
return self._response(request, 404, {"error": "not_found"})
|
|
if len(parts) == 3 and parts[2] == "test" and request.method == "GET":
|
|
return self._response(request, 200, {"reachable": True, "status": "healthy"})
|
|
if len(parts) == 4 and parts[2:] == ["tools", "refresh"] and request.method == "POST":
|
|
old_tools = len(self.tools.get(parts[1], []))
|
|
old_prompts = len(self.prompts.get(parts[1], []))
|
|
self.tools[parts[1]] = [
|
|
self._tool_payload(tool_name)
|
|
for tool_name in self._gateway_tools.get(str(stored["name"]), ())
|
|
]
|
|
self.prompts[parts[1]] = [
|
|
{"id": str(uuid4()), "name": prompt_name}
|
|
for prompt_name in self._gateway_prompts.get(str(stored["name"]), ())
|
|
]
|
|
return self._response(
|
|
request,
|
|
200,
|
|
{
|
|
"success": True,
|
|
"tools_added": max(0, len(self.tools[parts[1]]) - old_tools),
|
|
"tools_removed": 0,
|
|
"prompts_added": max(0, len(self.prompts[parts[1]]) - old_prompts),
|
|
"prompts_removed": 0,
|
|
},
|
|
)
|
|
if request.method == "GET":
|
|
return self._response(request, 200, deepcopy(stored))
|
|
if request.method == "PUT":
|
|
payload = json.loads(request.content)
|
|
stored.update(
|
|
{
|
|
"name": payload["name"],
|
|
"url": payload["url"],
|
|
"description": payload.get("description", ""),
|
|
"transport": payload.get("transport", "STREAMABLEHTTP"),
|
|
"enabled": payload.get("enabled", True),
|
|
"status": "active" if payload.get("enabled", True) else "inactive",
|
|
"passthrough_headers": payload.get("passthrough_headers", []),
|
|
}
|
|
)
|
|
return self._response(request, 200, deepcopy(stored))
|
|
if request.method == "DELETE":
|
|
del self.gateways[parts[1]]
|
|
return self._response(request, 204, {})
|
|
if len(parts) == 2 and parts[0] == "servers":
|
|
stored_server = self.servers.get(parts[1])
|
|
if stored_server is None:
|
|
return self._response(request, 404, {"error": "not_found"})
|
|
if request.method == "PUT":
|
|
payload = json.loads(request.content)
|
|
stored_server.update(
|
|
{
|
|
"name": payload["name"],
|
|
"associated_tool_ids": payload.get("associated_tools", []),
|
|
"associated_prompt_ids": payload.get("associated_prompts", []),
|
|
"visibility": payload.get("visibility", "private"),
|
|
}
|
|
)
|
|
return self._response(request, 200, deepcopy(stored_server))
|
|
return self._response(request, 404, {"error": "not_found"})
|