Watch
1
0
Fork
You've already forked RedFlag
0
RedFlag/agent/internal/localapi/client.go
Fimeg ffffe9b956 feat: FEAT-002 local agent API, desktop tray spine, screenshot handler
Slices 1-3 of the local agent IPC surface:
- Read model (local_status.go, loop wired): update counts, scanner status,
  check-in state, token receipt counts — no token material exposed
- Local IPC (localapi/): Unix socket (group=redflag-local, 0660) + Windows
  named pipe (SDDL: LocalSystem/Admins/RedFlagLocal); five read-only endpoints
- `redflag-agent -local-status` CLI probe of the local API surface
- Screenshot capture handler (screenshot.go, dispatch wired)
- Tauri desktop spine (desktop/): tray icon, left-click window, local IPC reader
- Desktop React entry (web/src/desktop/LocalAgentApp.tsx, index.desktop.html,
  vite.desktop.config.ts)
- Installer group provisioning: linux.sh creates redflag-local, sets
  SupplementaryGroups; windows.ps1 creates RedFlagLocal security group
- Server-side: screenshot receipt handler on agents, updates handler additions
- web/package.json: @tauri-apps/api + tauri CLI dev dep added
2026-06-08 17:08:58 -04:00

86 lines
2.1 KiB
Go

package localapi
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"time"
)
const localHTTPBaseURL = "http://redflag.local"
// ClientOptions configures local IPC client access. Defaults mirror the agent
// listener and remain platform-specific.
type ClientOptions struct {
UnixSocketPath string
WindowsPipeName string
Timeout time.Duration
}
// Snapshot is the compact local state needed by status probes and tray shells.
type Snapshot struct {
Identity IdentityResponse `json:"identity"`
Status StatusResponse `json:"status"`
}
// FetchSnapshot reads the local agent API over the platform IPC transport.
func FetchSnapshot(ctx context.Context, opts ClientOptions) (*Snapshot, error) {
httpClient := newLocalHTTPClient(opts)
identity, err := fetchJSON[IdentityResponse](ctx, httpClient, "/v1/identity")
if err != nil {
return nil, fmt.Errorf("fetch identity: %w", err)
}
status, err := fetchJSON[StatusResponse](ctx, httpClient, "/v1/status")
if err != nil {
return nil, fmt.Errorf("fetch status: %w", err)
}
return &Snapshot{
Identity: identity,
Status: status,
}, nil
}
func newLocalHTTPClient(opts ClientOptions) *http.Client {
timeout := opts.Timeout
if timeout == 0 {
timeout = 5 * time.Second
}
return &http.Client{
Timeout: timeout,
Transport: &http.Transport{
DialContext: dialLocalContext(opts),
DisableKeepAlives: true,
Proxy: nil,
},
}
}
func fetchJSON[T any](ctx context.Context, httpClient *http.Client, path string) (T, error) {
var value T
req, err := http.NewRequestWithContext(ctx, http.MethodGet, localHTTPBaseURL+path, nil)
if err != nil {
return value, err
}
resp, err := httpClient.Do(req)
if err != nil {
return value, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return value, fmt.Errorf("local API returned %s", resp.Status)
}
if err := json.NewDecoder(resp.Body).Decode(&value); err != nil {
return value, err
}
return value, nil
}
func dialLocalContext(opts ClientOptions) func(context.Context, string, string) (net.Conn, error) {
return platformDialLocalContext(opts)
}