server

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 17, 2026 License: MIT Imports: 24 Imported by: 0

README

computeid-server

A Postgres-backed implementation of the ComputeID API. Wire-compatible with the production endpoints documented in the Integration Guide PDF — the Go SDK and the Python SDK both work against it unchanged.

This server is suitable for local development, CI, and self-hosted deployments. The production API at https://api.aicomputeid.com remains the source of truth for managed deployments.


What it implements

All endpoints from the Integration Guide, plus one dev-only admin path:

Method Path Notes
POST /v1/agents/register Issues a passport. Generates an RSA-2048 / SHA-256 signature over canonical JSON of {passport_id, name, organization, capabilities, issued_at}.
GET /v1/agents/{id}/verify Returns status + signature_valid (independent — a revoked passport keeps a valid signature).
GET /v1/agents/{id}/capabilities/{name} granted + reason. After revoke, every capability returns granted=false, reason="passport_revoked".
POST /v1/agents/{id}/actions Append to the audit trail.
GET /v1/agents/{id}/actions?limit=N Read the audit trail (newest first, limit 1-500, default 50).
DELETE /v1/agents/{id}/revoke Idempotent revoke; writes a passport_revoked action.
GET /v1/agents?status= List passports, optional status filter.
POST /api/devices/register Issues a device passport (status=pending). Device code is allocated monotonically per type: GPU-001, GPU-002, ...
POST /api/devices/authenticate Exchanges device_code → HS256 JWT (1h TTL). Requires status=active.
GET /api/devices?status= List devices.
POST /api/devices/{id}/approve Dev-only. Moves a device from pending to active. Not in the production contract.
GET /health, GET /v1/status Liveness + DB ping.

Run with the existing leadpilot Postgres

# 1. Create the database
PGPASSWORD=leadpilot psql -h localhost -p 5439 -U leadpilot -d postgres \
  -c "CREATE DATABASE computeid OWNER leadpilot;"

# 2. Boot the server (migrations apply automatically, RSA key generates on first boot)
export DATABASE_URL=postgres://leadpilot:leadpilot@localhost:5439/computeid?sslmode=disable
export JWT_SECRET=local-dev-secret
go run ./cmd/computeid-server

# 3. Point the SDK at it
export COMPUTEID_API_BASE=http://localhost:8088
go run ./examples/serverbacked

Run with Docker Compose (isolated Postgres on :5440)

docker compose up --build
# Server on :8088, DB on :5440
export COMPUTEID_API_BASE=http://localhost:8088
go run ./examples/serverbacked

Environment

Var Required Default Purpose
DATABASE_URL yes Postgres connection string (pgxpool format).
JWT_SECRET yes HS256 secret for device JWTs.
PORT no 8088 HTTP listen port.
LOG_LEVEL no info debug / info / warn / error.

Schema

Five tables, single migration:

  • signing_keys — singleton RSA-2048 keypair persisted on first boot.
  • agents — passport rows + signed payload bytes (BYTEA so verify is byte-stable).
  • agent_actions — append-only audit log.
  • devices, device_counters — devices + per-type monotonic code allocator.
  • schema_migrations — applied versions.

See migrations/001_init.up.sql for the canonical definition.


Signing model

  • Key: RSA-2048, persisted in signing_keys so verification still works after restarts.
  • Algorithm: RSA-SHA256 (matches the production signature_algorithm string).
  • Payload: stable-order JSON of {passport_id, name, organization, capabilities, issued_at}.
  • Storage: exact bytes signed are persisted in agents.signed_payload BYTEA so the verifier can recompute the SHA-256 hash without re-serializing.

The signature is independent of status. Revoking a passport leaves signature_valid=true; the authorization rule is "status=="active" AND signature_valid==true".


What this server is not (yet)

  • The dev /approve endpoint is open unless ADMIN_TOKEN is set. When set, the endpoint requires a matching X-Admin-Token header.
  • No rate limiting.
  • No post-quantum signing — production roadmap.
  • No X-API-Key enforcement — keys are accepted and forwarded by the SDK but the server ignores them.

These are appropriate for local development and CI. For anything beyond that, talk to ComputeID.


Testing

# Fast: SDK unit tests only, no Postgres needed.
make test-unit

# Full: server + SDK↔server E2E tests against real Postgres.
make db-up     # one-time: creates 'computeid' and 'computeid_test' databases
make test      # uses one shared test DB with -p 1 to serialize binaries

# Or run the two packages in parallel against separate DBs (faster).
# Requires: createdb computeid_test_server && createdb computeid_test_e2e
go test -race -count=1 ./...

Per-test integration coverage (all hit real Postgres):

Package What it tests
server/migrate_test.go Idempotent re-runs; all 6 tables created.
server/signer_test.go RSA-2048 sign/verify; tampered payload + tampered signature both fail; key persists across reload.
server/integration_test.go Every HTTP endpoint end-to-end: register, verify (active+revoked → signature still valid), capabilities (granted/missing/revoked), log+list actions, revoke idempotency, list with status filter, device lifecycle, monotonic GPU code allocation, admin token gate.
root e2e_test.go Drives the SDK *Client against a live server; verifies the full PDF wire contract.

To skip integration tests on a machine without Postgres: COMPUTEID_SKIP_INTEGRATION=1 go test ./....

Documentation

Overview

Package server is a Postgres-backed ComputeID API implementation. Endpoints match the Integration Guide PDF wire contract so the Go SDK and the Python SDK both work against it unchanged.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Migrate

func Migrate(ctx context.Context, db DB) error

Migrate runs every up migration in lexical order. It's idempotent: a schema_migrations table tracks which versions have been applied.

Types

type ActionItem

type ActionItem struct {
	ActionID  string         `json:"action_id"`
	Action    string         `json:"action"`
	Details   map[string]any `json:"details,omitempty"`
	Outcome   string         `json:"outcome"`
	Timestamp time.Time      `json:"timestamp"`
}

ActionItem is a single audit row.

type ActionRow

type ActionRow struct {
	ActionID   string
	Action     string
	Details    map[string]any
	Outcome    string
	OccurredAt time.Time
}

ActionRow is the projected audit-log row.

type AgentPassportResponse

type AgentPassportResponse struct {
	PassportID         string    `json:"passport_id"`
	Name               string    `json:"name,omitempty"`
	Description        string    `json:"description,omitempty"`
	Organization       string    `json:"organization,omitempty"`
	Status             string    `json:"status"`
	PublicKey          string    `json:"public_key,omitempty"`
	Signature          string    `json:"signature"`
	SignatureAlgorithm string    `json:"signature_algorithm"`
	Capabilities       []string  `json:"capabilities"`
	IssuedAt           time.Time `json:"issued_at"`
}

AgentPassportResponse matches the 201 body documented in the Integration Guide PDF. Field order and JSON tags are stable.

type AgentRegisterRequest

type AgentRegisterRequest struct {
	Name         string   `json:"name"`
	Description  string   `json:"description"`
	Organization string   `json:"organization"`
	Capabilities []string `json:"capabilities"`
}

AgentRegisterRequest matches POST /v1/agents/register.

type AgentRow

type AgentRow struct {
	PassportID         string
	Name               string
	Description        string
	Organization       string
	Capabilities       []string
	Status             string
	PublicKeyPEM       string
	SignatureB64       string
	SignatureAlgorithm string
	SignedPayload      []byte
	IssuedAt           time.Time
	RevokedAt          *time.Time
	RevokeReason       string
}

AgentRow is the projected row type the handlers and store agree on.

type CanonicalPayload

type CanonicalPayload struct {
	PassportID   string   `json:"passport_id"`
	Name         string   `json:"name"`
	Organization string   `json:"organization"`
	Capabilities []string `json:"capabilities"`
	IssuedAt     string   `json:"issued_at"` // RFC3339Nano
}

CanonicalPayload is the subset of the passport that is signed at issuance. Fixed key order keeps signatures reproducible across runs.

type CapabilityResponse

type CapabilityResponse struct {
	Granted    bool           `json:"granted"`
	Capability string         `json:"capability,omitempty"`
	Scope      map[string]any `json:"scope,omitempty"`
	BoundAt    *time.Time     `json:"bound_at,omitempty"`
	Reason     string         `json:"reason,omitempty"`
}

CapabilityResponse matches GET /v1/agents/{id}/capabilities/{name}.

type CommandTag

type CommandTag interface {
	RowsAffected() int64
}

CommandTag describes the result of an Exec.

type Config

type Config struct {
	DB         DB
	JWTSecret  string
	AdminToken string
	Logger     *slog.Logger
}

Config configures New. AdminToken is optional; when non-empty, the dev-only admin endpoints (currently `POST /api/devices/{id}/approve`) require a matching `X-Admin-Token` header. When empty, they are open — appropriate only for local development.

type DB

type DB interface {
	Exec(ctx context.Context, sql string, args ...any) (CommandTag, error)
	Query(ctx context.Context, sql string, args ...any) (Rows, error)
	QueryRow(ctx context.Context, sql string, args ...any) Row
	Begin(ctx context.Context) (Tx, error)
	Close()
}

DB is the minimal driver surface used by the server (pgx-shaped, but abstracted so tests can substitute a fake later).

func ConnectPostgres

func ConnectPostgres(ctx context.Context, databaseURL string) (DB, error)

ConnectPostgres opens a pooled connection to a Postgres database.

type DeviceAuthRequest

type DeviceAuthRequest struct {
	DeviceCode string `json:"device_code"`
}

DeviceAuthRequest matches POST /api/devices/authenticate.

type DeviceAuthResponse

type DeviceAuthResponse struct {
	AccessToken string    `json:"access_token"`
	TokenType   string    `json:"token_type"`
	ExpiresAt   time.Time `json:"expires_at"`
}

DeviceAuthResponse mirrors the production JWT exchange.

type DeviceRegisterRequest

type DeviceRegisterRequest struct {
	Name       string `json:"name"`
	DeviceType string `json:"type"`
	IPAddress  string `json:"ip_address"`
}

DeviceRegisterRequest matches POST /api/devices/register.

type DeviceResponse

type DeviceResponse struct {
	DeviceID   string    `json:"device_id"`
	DeviceCode string    `json:"device_code"`
	Name       string    `json:"name"`
	DeviceType string    `json:"type"`
	IPAddress  string    `json:"ip_address,omitempty"`
	Status     string    `json:"status"`
	IssuedAt   time.Time `json:"issued_at"`
}

DeviceResponse matches the body returned by register / approve / list.

type DeviceRow

type DeviceRow struct {
	DeviceID   string
	DeviceCode string
	Name       string
	DeviceType string
	IPAddress  string
	Status     string
	IssuedAt   time.Time
	ApprovedAt *time.Time
}

DeviceRow is the projected device row.

type ListActionsResponse

type ListActionsResponse struct {
	Actions []ActionItem `json:"actions"`
}

ListActionsResponse matches GET /v1/agents/{id}/actions.

type ListAgentsResponse

type ListAgentsResponse struct {
	Agents []AgentPassportResponse `json:"agents"`
}

ListAgentsResponse matches GET /v1/agents.

type LogActionRequest

type LogActionRequest struct {
	Action  string         `json:"action"`
	Details map[string]any `json:"details"`
	Outcome string         `json:"outcome"`
}

LogActionRequest matches POST /v1/agents/{id}/actions.

type RevokeRequest

type RevokeRequest struct {
	Reason string `json:"reason"`
}

RevokeRequest matches DELETE /v1/agents/{id}/revoke.

type Row

type Row interface {
	Scan(dest ...any) error
}

Row is a single-row result.

type Rows

type Rows interface {
	Next() bool
	Scan(dest ...any) error
	Err() error
	Close()
}

Rows is a multi-row iterator.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server is the HTTP handler bundle.

func New

func New(ctx context.Context, cfg Config) (*Server, error)

New constructs a Server. It loads or generates the RSA signing key on startup so the first call after a clean install issues a real signature.

func (*Server) Handler

func (s *Server) Handler() http.Handler

Handler returns the HTTP handler with all routes wired up and the request logger applied.

type Signer

type Signer struct {
	// contains filtered or unexported fields
}

Signer issues RSA-2048 / SHA-256 signatures over canonical passport JSON. Matches the signature_algorithm == "RSA-SHA256" string returned by the production ComputeID API.

func LoadOrInitSigner

func LoadOrInitSigner(ctx context.Context, db DB) (*Signer, error)

LoadOrInitSigner reads the singleton RSA key from the signing_keys table, or generates and persists a fresh one if the table is empty.

func (*Signer) Algorithm

func (s *Signer) Algorithm() string

Algorithm returns the signature_algorithm string ("RSA-SHA256").

func (*Signer) PublicKeyPEM

func (s *Signer) PublicKeyPEM() string

PublicKeyPEM returns the PEM-encoded public key issued passports carry.

func (*Signer) Sign

func (s *Signer) Sign(p CanonicalPayload) (sigB64 string, signedJSON []byte, err error)

Sign returns a base64-encoded RSA-PKCS1v15 signature over a canonical JSON of the payload, plus the JSON bytes that were actually signed (stored in the agents row so Verify can recompute later).

func (*Signer) Verify

func (s *Signer) Verify(signedJSON []byte, sigB64 string) bool

Verify returns true iff sigB64 was produced by this signer over signedJSON.

type Tx

type Tx interface {
	Exec(ctx context.Context, sql string, args ...any) (CommandTag, error)
	Query(ctx context.Context, sql string, args ...any) (Rows, error)
	QueryRow(ctx context.Context, sql string, args ...any) Row
	Commit(ctx context.Context) error
	Rollback(ctx context.Context) error
}

Tx is a database transaction.

type VerifyResponse

type VerifyResponse struct {
	PassportID     string     `json:"passport_id"`
	Status         string     `json:"status"`
	SignatureValid bool       `json:"signature_valid"`
	Capabilities   []string   `json:"capabilities"`
	IssuedAt       time.Time  `json:"issued_at"`
	RevokedAt      *time.Time `json:"revoked_at,omitempty"`
}

VerifyResponse matches GET /v1/agents/{id}/verify.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL