Explanation: Deep dive into how the ACP Gateway integrates SEA-Forge™ with Zed IDE and external services.
The ACP (Agent Communication Protocol) Gateway is SEA-Forge™’s bridge between Zed IDE and the semantic execution stack. It implements a JSON-RPC 2.0 server that exposes SEA™ capabilities as IDE-native tools.
Architecture Location: Integration Adapter Layer (per AGENTS.md §Integration Adapter Exception)
Specification References:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
┌─────────────────────────────────────────────────────────────────┐
│ Zed IDE │
│ (sends JSON-RPC requests via stdio) │
└────────────────────────────┬────────────────────────────────────┘
│
│ JSON-RPC 2.0 (stdio)
│
┌────────────────────────────▼────────────────────────────────────┐
│ ACP Gateway │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Message │ │ Tool │ │ Capability │ │
│ │ Handler │ │ Executor │ │ Registry │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └─────────────────┴─────────────────┘ │
│ │ │
└───────────────────────────┼──────────────────────────────────────┘
│
┌───────────────────┼───────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ LLM Provider │ │ Knowledge │ │ Validation │
│ Service │ │ Graph │ │ + Pipeline │
│ (HTTP API) │ │ (Oxigraph/ │ │ (subprocess)│
│ │ │ rdflib) │ │ │
└──────────────┘ └──────────────┘ └──────────────┘
SDS-049 SDS-003 SDS-002/021
Problem: External services may not be running (LLM, Oxigraph).
Solution: Multi-tier fallback strategy for each capability.
Example - Chat Completion:
1
2
3
4
5
1. Try: HTTP call to LLM service (localhost:8000)
↓ fails
2. Try: Return informative echo message
↓ always succeeds
3. Result: User gets response + guidance to start service
Example - Knowledge Graph:
1
2
3
4
5
6
1. Try: HTTP call to Oxigraph (localhost:7878)
↓ fails
2. Try: In-memory RDF via rdflib + .snapshot.json files
↓ fails
3. Try: Return empty results + error message
↓ always succeeds
Problem: Not all users need all integrations.
Solution: Conditional imports with runtime checks.
1
2
3
4
5
6
7
8
try:
import httpx
except ImportError:
httpx = None # type: ignore
# Later in code
if httpx is None:
return {"error": "Install httpx: pip install httpx"}
Benefits:
Problem: Zed IDE expects consistent JSON-RPC responses.
Solution: Wrapper pattern maintains contract even when services fail.
1
2
3
4
5
6
# Always returns valid JSON-RPC response
{
"success": bool,
"result": {...} | None,
"error": str | None
}
Invariant: No exceptions escape to Zed IDE (caught and converted to error responses).
Endpoint: POST http://localhost:8000/v1/chat/completions
Request:
1
2
3
4
5
6
7
8
{
"messages": [
{"role": "user", "content": "Validate specs in docs/specs/orders/"}
],
"model": "ollama/llama3.2",
"temperature": 0.7,
"max_tokens": 500
}
Response:
1
2
3
4
5
6
7
8
9
10
11
12
{
"id": "chatcmpl-...",
"choices": [
{
"message": {
"role": "assistant",
"content": "I'll validate the specifications..."
},
"finish_reason": "stop"
}
]
}
Implementation:
1
2
3
4
5
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.post(
f"{llm_service_url}/v1/chat/completions",
json={"messages": messages, "model": model}
)
Fallback: Echo response with service start instructions.
Oxigraph Endpoint: POST http://localhost:7878/query
Request:
1
2
3
4
5
POST /query HTTP/1.1
Accept: application/sparql-results+json
Content-Type: application/x-www-form-urlencoded
query=SELECT ?entity WHERE { ?entity rdf:type sea:Entity }
Response:
1
2
3
4
5
6
7
{
"results": {
"bindings": [
{"entity": {"type": "uri", "value": "http://sea-forge.com/concept/orders/Entity/Order"}}
]
}
}
Fallback - In-Memory RDF:
1
2
3
4
5
6
7
graph = rdflib.Graph()
for snapshot_file in glob.glob(f"docs/specs/{context}/*.snapshot.json"):
snapshot = json.load(open(snapshot_file))
turtle_rdf = snapshot["rdf_turtle"]
graph.parse(data=turtle_rdf, format="turtle")
results = graph.query(sparql) # Execute locally
Fallback Chain:
Subprocess Call: python tools/validate_sds.py <file>
Implementation:
1
2
3
4
5
6
7
8
9
result = subprocess.run(
[sys.executable, "tools/validate_sds.py", path],
capture_output=True,
text=True,
timeout=30
)
if result.returncode != 0:
errors.append(result.stderr.strip())
Why Subprocess?
Just Command: just pipeline <context>
Implementation:
1
2
3
4
5
6
7
8
9
10
11
12
13
result = subprocess.run(
["just", "pipeline", spec_id],
cwd=repo_root,
capture_output=True,
timeout=120
)
# Parse generated files from output
output_lines = result.stdout.strip().split('\n')
generated_files = [
line for line in output_lines
if 'generated' in line.lower()
]
Why Just Command?
Every integration point follows this pattern:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
try:
# Primary: Attempt service call
result = await call_service(...)
if result.status_code == 200:
return success_response(result)
else:
# Service returned error
return error_response_with_guidance(result)
except ConnectionError:
# Service not running
return {"error": "Service unavailable. Start with: uvicorn ..."}
except TimeoutError:
# Service too slow
return {"error": "Service timeout. Check service health."}
except ImportError:
# Dependency missing
return {"error": "Install dependency: pip install httpx"}
except Exception as e:
# Unexpected error
return {"error": f"Unexpected error: {str(e)}"}
Never:
Always:
| Service | Timeout | Rationale |
|---|---|---|
| LLM | 30s | Chat completion typically < 10s |
| Validation | 30s | File I/O + schema validation |
| Pipeline | 120s | Large contexts may generate 50+ files |
| Oxigraph | 10s | SPARQL queries are fast or hanging |
Current: No caching (stateless gateway)
Future Optimization:
Current: Serial execution per request (simple, correct)
Future Optimization:
Located in tests/acp/test_acp_gateway.py:
1
2
3
4
5
6
7
async def test_handle_send_message():
gateway = ACPGateway()
result = await gateway._handle_send_message({
"message": {"role": "user", "content": "hello"}
})
assert "response" in result
assert result["response"]["role"] == "agent"
Coverage:
Not Covered (integration tests):
Recommended: Add tests/acp/test_integration.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@pytest.mark.integration
async def test_llm_service_integration():
"""Test with real LLM service running."""
gateway = ACPGateway()
# Start LLM service in background
proc = subprocess.Popen(["uvicorn", "services.llm-provider.main:app"])
await asyncio.sleep(2) # Wait for startup
try:
result = await gateway._handle_send_message({
"message": {"role": "user", "content": "hello"}
})
assert result["response"]["content"] != "Service unavailable"
finally:
proc.kill()
1
2
3
4
5
6
7
8
9
# Terminal 1: Start LLM service
cd services/llm-provider
uvicorn main:app --reload
# Terminal 2: Start Oxigraph
docker run -p 7878:7878 oxigraph/oxigraph serve
# Terminal 3: Run ACP Gateway
python -m libs.sea.adapters.acp.src.main
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# deployment.yaml
apiVersion: v1
kind: Service
metadata:
name: acp-gateway
spec:
selector:
app: acp-gateway
ports:
- port: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: acp-gateway
spec:
replicas: 2
template:
spec:
containers:
- name: acp-gateway
image: sea-forge/acp-gateway:latest
env:
- name: LLM_PROVIDER_URL
value: "http://llm-provider:8000"
- name: OXIGRAPH_URL
value: "http://oxigraph:7878"
Future: Add OTel instrumentation per SDS-030
1
2
3
4
5
6
7
8
from opentelemetry import trace
tracer = trace.get_tracer("acp-gateway")
async def _handle_send_message(self, params):
with tracer.start_as_current_span("acp.send_message") as span:
span.set_attribute("message.role", params["message"]["role"])
# ... implementation
Recommended:
acp_requests_total{method, status} - Counteracp_request_duration_seconds{method} - Histogramacp_service_errors_total{service} - CounterStatus: ACP Gateway runs on stdio (local process)
Threat Model: Zed IDE is trusted (user’s local machine)
Future: If exposing via HTTP, add JWT authentication
Status: All capabilities exposed to Zed IDE
Mitigation: Capabilities are read-only or sandbox-safe
**/src/gen/** only (per codegen rules)Risk: User-controlled strings in subprocess calls
Mitigation:
1
2
3
4
5
# BAD: Shell injection risk
subprocess.run(f"just pipeline {user_input}", shell=True)
# GOOD: Array args (no shell)
subprocess.run(["just", "pipeline", user_input], shell=False)
httpx: Pinned to 0.28.1, audit for CVEs rdflib: Pinned to 7.5.0, audit for CVEs
Policy: Update dependencies monthly via Dependabot
The ACP Gateway demonstrates SEA-Forge™’s graceful degradation philosophy:
This design enables:
Result: Zed IDE integration works out-of-the-box, scales to production.