AX Quality Gates (RFC-025):
- Eliminate os/exec from all test + production code (12+ files)
- Eliminate encoding/json from all test files (15 files, 66 occurrences)
- Eliminate os from all test files except TestMain (Go runtime contract)
- Eliminate path/filepath, net/url from all files
- String concat: 39 violations replaced with core.Concat()
- Test naming AX-7: 264 test functions renamed across all 6 packages
- Example test 1:1 coverage complete
Core Features Adopted:
- Task Composition: agent.completion pipeline (QA → PR → Verify → Ingest → Poke)
- PerformAsync: completion pipeline runs with WaitGroup + progress tracking
- Config: agents.yaml loaded once, feature flags (auto-qa/pr/merge/ingest)
- Named Locks: c.Lock("drain") for queue serialisation
- Registry: workspace state with cross-package QUERY access
- QUERY: c.QUERY(WorkspaceQuery{Status: "running"}) for cross-service queries
- Action descriptions: 25+ Actions self-documenting
- Data mounts: prompts/tasks/flows/personas/workspaces via c.Data()
- Content Actions: agentic.prompt/task/flow/persona callable via IPC
- Drive endpoints: forge + brain registered with tokens
- Drive REST helpers: DriveGet/DrivePost/DriveDo for Drive-aware HTTP
- HandleIPCEvents: auto-discovered by WithService (no manual wiring)
- Entitlement: frozen-queue gate on write Actions
- CLI dispatch: workspace dispatch wired to real dispatch method
- CLI: --quiet/-q and --debug/-d global flags
- CLI: banner, version, check (with service/action/command counts), env
- main.go: minimal — 5 services + c.Run(), no os import
- cmd tests: 84.2% coverage (was 0%)
Co-Authored-By: Virgil <virgil@lethean.io>
304 lines
9.2 KiB
Go
304 lines
9.2 KiB
Go
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
// HTTP transport for Core API streams.
|
|
// This is the ONE file in core/agent that imports net/http.
|
|
// All other files use the exported helpers: HTTPGet, HTTPPost, HTTPCall.
|
|
|
|
package agentic
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"time"
|
|
|
|
core "dappco.re/go/core"
|
|
)
|
|
|
|
// defaultClient is the shared HTTP client for all transport calls.
|
|
var defaultClient = &http.Client{Timeout: 30 * time.Second}
|
|
|
|
// httpStream implements core.Stream over HTTP request/response.
|
|
type httpStream struct {
|
|
client *http.Client
|
|
url string
|
|
token string
|
|
method string
|
|
response []byte
|
|
}
|
|
|
|
func (s *httpStream) Send(data []byte) error {
|
|
req, err := http.NewRequestWithContext(context.Background(), s.method, s.url, core.NewReader(string(data)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Accept", "application/json")
|
|
if s.token != "" {
|
|
req.Header.Set("Authorization", core.Concat("token ", s.token))
|
|
}
|
|
|
|
resp, err := s.client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
r := core.ReadAll(resp.Body)
|
|
if !r.OK {
|
|
return core.E("httpStream.Send", "failed to read response", nil)
|
|
}
|
|
s.response = []byte(r.Value.(string))
|
|
return nil
|
|
}
|
|
|
|
func (s *httpStream) Receive() ([]byte, error) {
|
|
return s.response, nil
|
|
}
|
|
|
|
func (s *httpStream) Close() error {
|
|
return nil
|
|
}
|
|
|
|
// RegisterHTTPTransport registers the HTTP/HTTPS protocol handler with Core API.
|
|
//
|
|
// agentic.RegisterHTTPTransport(c)
|
|
func RegisterHTTPTransport(c *core.Core) {
|
|
factory := func(handle *core.DriveHandle) (core.Stream, error) {
|
|
token := handle.Options.String("token")
|
|
return &httpStream{
|
|
client: defaultClient,
|
|
url: handle.Transport,
|
|
token: token,
|
|
method: "POST",
|
|
}, nil
|
|
}
|
|
c.API().RegisterProtocol("http", factory)
|
|
c.API().RegisterProtocol("https", factory)
|
|
}
|
|
|
|
// --- REST helpers — all HTTP in core/agent routes through these ---
|
|
|
|
// HTTPGet performs a GET request. Returns Result{Value: string (response body), OK: bool}.
|
|
// Auth is "token {token}" for Forge, "Bearer {token}" for Brain.
|
|
//
|
|
// r := agentic.HTTPGet(ctx, "https://forge.lthn.ai/api/v1/repos", "my-token", "token")
|
|
func HTTPGet(ctx context.Context, url, token, authScheme string) core.Result {
|
|
return httpDo(ctx, "GET", url, "", token, authScheme)
|
|
}
|
|
|
|
// HTTPPost performs a POST request with a JSON body. Returns Result{Value: string, OK: bool}.
|
|
//
|
|
// r := agentic.HTTPPost(ctx, url, core.JSONMarshalString(payload), token, "token")
|
|
func HTTPPost(ctx context.Context, url, body, token, authScheme string) core.Result {
|
|
return httpDo(ctx, "POST", url, body, token, authScheme)
|
|
}
|
|
|
|
// HTTPPatch performs a PATCH request with a JSON body.
|
|
//
|
|
// r := agentic.HTTPPatch(ctx, url, body, token, "token")
|
|
func HTTPPatch(ctx context.Context, url, body, token, authScheme string) core.Result {
|
|
return httpDo(ctx, "PATCH", url, body, token, authScheme)
|
|
}
|
|
|
|
// HTTPDelete performs a DELETE request.
|
|
//
|
|
// r := agentic.HTTPDelete(ctx, url, body, token, "Bearer")
|
|
func HTTPDelete(ctx context.Context, url, body, token, authScheme string) core.Result {
|
|
return httpDo(ctx, "DELETE", url, body, token, authScheme)
|
|
}
|
|
|
|
// HTTPDo performs an HTTP request with the specified method.
|
|
//
|
|
// r := agentic.HTTPDo(ctx, "PUT", url, body, token, "token")
|
|
func HTTPDo(ctx context.Context, method, url, body, token, authScheme string) core.Result {
|
|
return httpDo(ctx, method, url, body, token, authScheme)
|
|
}
|
|
|
|
// --- Drive-aware REST helpers — route through c.Drive() for endpoint resolution ---
|
|
|
|
// DriveGet performs a GET request using a named Drive endpoint.
|
|
// Reads base URL and token from the Drive handle registered in Core.
|
|
//
|
|
// r := DriveGet(c, "forge", "/api/v1/repos/core/go-io", "token")
|
|
func DriveGet(c *core.Core, drive, path, authScheme string) core.Result {
|
|
base, token := driveEndpoint(c, drive)
|
|
if base == "" {
|
|
return core.Result{Value: core.E("DriveGet", core.Concat("drive not found: ", drive), nil), OK: false}
|
|
}
|
|
return httpDo(context.Background(), "GET", core.Concat(base, path), "", token, authScheme)
|
|
}
|
|
|
|
// DrivePost performs a POST request using a named Drive endpoint.
|
|
//
|
|
// r := DrivePost(c, "forge", "/api/v1/repos/core/go-io/issues", body, "token")
|
|
func DrivePost(c *core.Core, drive, path, body, authScheme string) core.Result {
|
|
base, token := driveEndpoint(c, drive)
|
|
if base == "" {
|
|
return core.Result{Value: core.E("DrivePost", core.Concat("drive not found: ", drive), nil), OK: false}
|
|
}
|
|
return httpDo(context.Background(), "POST", core.Concat(base, path), body, token, authScheme)
|
|
}
|
|
|
|
// DriveDo performs an HTTP request using a named Drive endpoint.
|
|
//
|
|
// r := DriveDo(c, "forge", "PATCH", "/api/v1/repos/core/go-io/pulls/5", body, "token")
|
|
func DriveDo(c *core.Core, drive, method, path, body, authScheme string) core.Result {
|
|
base, token := driveEndpoint(c, drive)
|
|
if base == "" {
|
|
return core.Result{Value: core.E("DriveDo", core.Concat("drive not found: ", drive), nil), OK: false}
|
|
}
|
|
return httpDo(context.Background(), method, core.Concat(base, path), body, token, authScheme)
|
|
}
|
|
|
|
// driveEndpoint reads base URL and token from a named Drive handle.
|
|
func driveEndpoint(c *core.Core, name string) (base, token string) {
|
|
r := c.Drive().Get(name)
|
|
if !r.OK {
|
|
return "", ""
|
|
}
|
|
h := r.Value.(*core.DriveHandle)
|
|
return h.Transport, h.Options.String("token")
|
|
}
|
|
|
|
// httpDo is the single HTTP execution point. Every HTTP call in core/agent routes here.
|
|
func httpDo(ctx context.Context, method, url, body, token, authScheme string) core.Result {
|
|
var req *http.Request
|
|
var err error
|
|
|
|
if body != "" {
|
|
req, err = http.NewRequestWithContext(ctx, method, url, core.NewReader(body))
|
|
} else {
|
|
req, err = http.NewRequestWithContext(ctx, method, url, nil)
|
|
}
|
|
if err != nil {
|
|
return core.Result{OK: false}
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Accept", "application/json")
|
|
if token != "" {
|
|
if authScheme == "" {
|
|
authScheme = "token"
|
|
}
|
|
req.Header.Set("Authorization", core.Concat(authScheme, " ", token))
|
|
}
|
|
|
|
resp, err := defaultClient.Do(req)
|
|
if err != nil {
|
|
return core.Result{OK: false}
|
|
}
|
|
|
|
r := core.ReadAll(resp.Body)
|
|
if !r.OK {
|
|
return core.Result{OK: false}
|
|
}
|
|
|
|
return core.Result{Value: r.Value.(string), OK: resp.StatusCode < 400}
|
|
}
|
|
|
|
// --- MCP Streamable HTTP Transport ---
|
|
|
|
// mcpInitialize performs the MCP initialise handshake over Streamable HTTP.
|
|
// Returns the session ID from the Mcp-Session-Id header.
|
|
func mcpInitialize(ctx context.Context, url, token string) (string, error) {
|
|
initReq := map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "initialize",
|
|
"params": map[string]any{
|
|
"protocolVersion": "2025-03-26",
|
|
"capabilities": map[string]any{},
|
|
"clientInfo": map[string]any{
|
|
"name": "core-agent-remote",
|
|
"version": "0.2.0",
|
|
},
|
|
},
|
|
}
|
|
|
|
body := core.JSONMarshalString(initReq)
|
|
req, err := http.NewRequestWithContext(ctx, "POST", url, core.NewReader(body))
|
|
if err != nil {
|
|
return "", core.E("mcpInitialize", "create request", nil)
|
|
}
|
|
mcpHeaders(req, token, "")
|
|
|
|
resp, err := defaultClient.Do(req)
|
|
if err != nil {
|
|
return "", core.E("mcpInitialize", "request failed", nil)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
return "", core.E("mcpInitialize", core.Sprintf("HTTP %d", resp.StatusCode), nil)
|
|
}
|
|
|
|
sessionID := resp.Header.Get("Mcp-Session-Id")
|
|
|
|
// Drain SSE response
|
|
drainSSE(resp)
|
|
|
|
// Send initialised notification
|
|
notif := core.JSONMarshalString(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"method": "notifications/initialized",
|
|
})
|
|
notifReq, _ := http.NewRequestWithContext(ctx, "POST", url, core.NewReader(notif))
|
|
mcpHeaders(notifReq, token, sessionID)
|
|
notifResp, err := defaultClient.Do(notifReq)
|
|
if err == nil {
|
|
notifResp.Body.Close()
|
|
}
|
|
|
|
return sessionID, nil
|
|
}
|
|
|
|
// mcpCall sends a JSON-RPC request and returns the parsed response.
|
|
func mcpCall(ctx context.Context, url, token, sessionID string, body []byte) ([]byte, error) {
|
|
req, err := http.NewRequestWithContext(ctx, "POST", url, core.NewReader(string(body)))
|
|
if err != nil {
|
|
return nil, core.E("mcpCall", "create request", nil)
|
|
}
|
|
mcpHeaders(req, token, sessionID)
|
|
|
|
resp, err := defaultClient.Do(req)
|
|
if err != nil {
|
|
return nil, core.E("mcpCall", "request failed", nil)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
return nil, core.E("mcpCall", core.Sprintf("HTTP %d", resp.StatusCode), nil)
|
|
}
|
|
|
|
return readSSEData(resp)
|
|
}
|
|
|
|
// readSSEData reads an SSE response and extracts JSON from data: lines.
|
|
func readSSEData(resp *http.Response) ([]byte, error) {
|
|
r := core.ReadAll(resp.Body)
|
|
if !r.OK {
|
|
return nil, core.E("readSSEData", "failed to read response", nil)
|
|
}
|
|
for _, line := range core.Split(r.Value.(string), "\n") {
|
|
if core.HasPrefix(line, "data: ") {
|
|
return []byte(core.TrimPrefix(line, "data: ")), nil
|
|
}
|
|
}
|
|
return nil, core.E("readSSEData", "no data in SSE response", nil)
|
|
}
|
|
|
|
// mcpHeaders applies standard MCP HTTP headers.
|
|
func mcpHeaders(req *http.Request, token, sessionID string) {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Accept", "application/json, text/event-stream")
|
|
if token != "" {
|
|
req.Header.Set("Authorization", core.Concat("Bearer ", token))
|
|
}
|
|
if sessionID != "" {
|
|
req.Header.Set("Mcp-Session-Id", sessionID)
|
|
}
|
|
}
|
|
|
|
// drainSSE reads and discards an SSE response body.
|
|
func drainSSE(resp *http.Response) {
|
|
core.ReadAll(resp.Body)
|
|
}
|