server

package
v0.3.0-alpha.6 Latest Latest
Warning

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

Go to latest
Published: Jul 6, 2026 License: Apache-2.0 Imports: 39 Imported by: 0

Documentation

Overview

Package server provides the Gramaton HTTP server (daemon). It wraps a core.Engine with HTTP handlers, concurrency control via the engine's RWMutex, and lifecycle management (idle timeout, graceful shutdown).

Index

Constants

View Source
const (
	// MCPToolRead marks tools that only read store state (or write
	// strictly outside the data dir); they stay registered on a
	// frozen store.
	MCPToolRead = "read"

	// MCPToolWrite marks tools with at least one action that mutates
	// store state; they are not registered on a frozen store.
	MCPToolWrite = "write"
)

Store-level read-only classification of the MCP tool surface.

Every MCP tool name registered by either MCP surface -- the server's own bindings (server/bindings_*.go, served over /mcp and stdio) and the CLI proxy (cli/mcp_proxy_*.go, served by `gramaton mcp`) -- must appear in mcpToolAccess below, classified as read or write. Against a frozen store the write-classified tools are stripped from the registration (Server.registerMCPTools here, registerProxyToolsReadOnly in cli/mcp_proxy.go), so an agent attached to a read-only store never sees a tool it cannot use. The api-layer guards (api/readonly.go) remain the enforcement backstop for anything that reaches a write path anyway; this filtering is presentation, not security.

MANUAL INVARIANT, cross-checked against api/readonly_guard_test.go (no automated assertion is practical: that classification lives in a _test.go file and keys on api method names, not tool names): a tool is MCPToolWrite iff at least one operation reachable through it is classified guardWrite there. Judgment calls, recorded:

  • gramaton_branch: write. Its `list` action is a read, but create/checkout/merge/discard rewrite HEAD/refs (guardWrite), and a single mutating action makes the whole tool write.
  • gramaton_backup: read. The MCP tool exposes only status + create, both guardRead -- they write strictly OUTSIDE the data dir, and exporting a frozen store is how it is shared. Restore and import are not MCP-exposed.
  • gramaton_save_batch_cancel: read, matching SaveBatchCancel's guardRead classification (jobs.db is a derived local cache that stays writable on a frozen store; no batch can start on one, so cancel is leftover-job cleanup, not knowledge mutation).
  • gramaton_curation: write. action=status is a read, but trigger/dry_run apply the deterministic phase (guardWrite).
  • gramaton_intake: write; registered only on the server surface (the CLI proxy deliberately excludes it -- see the comment in cli/mcp_proxy.go's registerProxyTools).

When a NEW tool trips TestMCPToolAccessCoversServerSurface (server) or TestProxyToolAccessClassification (cli): decide whether ANY action the tool exposes reaches a guardWrite api operation. If so, add it as MCPToolWrite; otherwise MCPToolRead. Never leave a tool unclassified -- an unclassified write tool would stay visible on frozen stores.

Variables

This section is empty.

Functions

func IsProcessAlive

func IsProcessAlive(pid int) bool

IsProcessAlive reports whether a process with the given PID exists. On Unix, os.FindProcess always succeeds for any PID, so we probe liveness by sending signal 0: the kernel validates the PID exists and we have permission to signal it, but delivers nothing.

func MCPRemoteExcludedToolNames

func MCPRemoteExcludedToolNames() []string

MCPRemoteExcludedToolNames returns the tools removed from the trimmed remote MCP surface, sorted. Removing a name that was never registered is a no-op.

func MCPToolAccess

func MCPToolAccess(name string) (access string, ok bool)

MCPToolAccess returns the read/write classification for an MCP tool name, and ok=false for names that have not been classified.

func MCPWriteToolNames

func MCPWriteToolNames() []string

MCPWriteToolNames returns every write-classified MCP tool name, sorted. Callers pass the result to mcp.Server.RemoveTools when the store is frozen; removing a name that was never registered is a no-op, so the proxy (which registers a subset of the server surface) can use the same list.

func RegisterMCPProxy

func RegisterMCPProxy(cfgDir string) error

RegisterMCPProxy writes a registration file for the current process into the store's config dir. Callers treat failure as non-fatal bookkeeping: a proxy that cannot register still serves tool calls.

func RemoveMCPProxy

func RemoveMCPProxy(cfgDir string, pid int)

RemoveMCPProxy deletes the registration file for a proxy PID. Removing a missing file is a no-op: SIGKILLed proxies never get to clean up, and ListMCPProxies may already have pruned the entry.

func RemoveServerInfo

func RemoveServerInfo(cfgDir string)

RemoveServerInfo removes the server info file from a config directory.

Types

type Config

type Config struct {
	Port        int           `yaml:"port"`
	Bind        string        `yaml:"bind"`
	IdleTimeout time.Duration `yaml:"idle_timeout"`
	ConfigDir   string        // runtime, not from YAML
	StoreName   string        // runtime, empty = default unnamed store

	// Remote is the resolved remote-access configuration. Zero value
	// (Enabled false) keeps the server loopback-only. cli/serve.go
	// populates it -- resolving the token and certificate paths and
	// failing closed on missing material -- so the server itself
	// never touches config files or secret resolution.
	Remote RemoteRuntime
}

Config holds server configuration.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns server config with sensible defaults.

type CurationStatus

type CurationStatus struct {
	PendingCount      int        `json:"pending_count"`
	Overdue           bool       `json:"overdue"`
	ConceptCandidates int        `json:"concept_candidates,omitempty"`
	StaleCount        int        `json:"stale_count,omitempty"`
	OrphanCount       int        `json:"orphan_count,omitempty"`
	LastCurated       *time.Time `json:"last_curated,omitempty"`
	Autonomous        bool       `json:"autonomous,omitempty"`
	LLMCallsToday     int        `json:"llm_calls_today,omitempty"`
	LLMDailyCap       int        `json:"llm_daily_cap,omitempty"`
	LLMCapPct         int        `json:"llm_cap_pct,omitempty"`
	Paused            bool       `json:"paused,omitempty"`
	PauseReason       string     `json:"pause_reason,omitempty"`
}

CurationStatus reports pending curation state.

type ErrorDetail

type ErrorDetail struct {
	Code      string `json:"code"`
	Message   string `json:"message"`
	Retryable bool   `json:"retryable"`
	// RetryAfter is the suggested wait in seconds before retrying, set
	// only on 429 responses. It mirrors the Retry-After HTTP header for
	// transports (notably the CLI proxy) that surface the body but drop
	// response headers, so the agent still learns how long to back off.
	RetryAfter int `json:"retry_after,omitempty"`
}

ErrorDetail contains error information.

func (*ErrorDetail) Error

func (e *ErrorDetail) Error() string

Error makes ErrorDetail satisfy the error interface so CLI clients can return it verbatim (rather than collapsing to a plain fmt.Errorf) and downstream code can recover Code/Retryable via errors.As. Transports that care only about a human string still get one via "%s: %s".

type ErrorResponse

type ErrorResponse struct {
	Error    ErrorDetail    `json:"error"`
	Curation CurationStatus `json:"curation,omitzero"`
	// StoreReadonly mirrors ResponseEnvelope.StoreReadonly: emitted
	// (true) only when the store is read-only, so a rejected write
	// carries the reason on the envelope as well as in the error.
	StoreReadonly bool `json:"store_readonly,omitempty"`
}

ErrorResponse is the standard error wrapper. Includes the curation envelope so error responses carry the same backlog signal as successful ones.

type MCPProxyInfo

type MCPProxyInfo struct {
	PID       int       `json:"pid"`
	StartedAt time.Time `json:"started_at"`
	// Binary is the resolved executable path of the proxy process,
	// from os.Executable(). Surfaced by `gramaton status` so a proxy
	// that predates a binary upgrade is visible. Gramaton never
	// restarts proxies -- the spawning harness owns respawning.
	Binary string `json:"binary,omitempty"`
}

MCPProxyInfo describes a registered `gramaton mcp` proxy process. It is written to <config-dir>/mcp/<pid>.json when a proxy starts and removed on clean exit. Proxies are spawned by MCP clients (not by the server), so this registry is the only way the rest of the CLI can discover them: `gramaton stop` reaps registered proxies and `gramaton status` lists them.

func ListMCPProxies

func ListMCPProxies(cfgDir string) []MCPProxyInfo

ListMCPProxies returns the registered proxies whose PID is still alive, sorted by PID. Entries that are unparseable or refer to a dead process are pruned from disk as the list is read: a proxy killed without cleanup (SIGKILL, harness crash) leaves a stale file behind, and this is where it gets collected.

Liveness is a PID-existence probe only. A PID recycled by an unrelated process passes this check; callers that act on entries (notably the stop command's reaper) must verify the process identity before signalling it.

func ListMCPProxiesNoPrune

func ListMCPProxiesNoPrune(cfgDir string) []MCPProxyInfo

ListMCPProxiesNoPrune is the read-only variant of ListMCPProxies: the same live-proxy filtering, but stale or unparseable entries are left on disk instead of being collected. For inventory-style callers (uninstall's dry-run and pre-confirmation survey) that must not mutate anything on disk.

type RemoteRuntime

type RemoteRuntime struct {
	Enabled  bool
	BindAddr string // empty = all interfaces
	Port     int
	Token    string // resolved shared secret; required when Enabled
	CertFile string // resolved cert path; required when Enabled
	KeyFile  string // resolved key path; required when Enabled
	AdminOps bool   // open path-taking admin ops to authenticated remotes

	// WriteRate, WriteBurst, and MaxConcurrentWrites are the resolved
	// admission-control limits for the remote write path (see
	// config.RemoteServerConfig.ResolvedWriteLimits). A non-positive
	// value disables that gate. All three zero -- the zero
	// RemoteRuntime, i.e. remote access disabled -- means no admission
	// control, which is correct because only loopback callers reach a
	// loopback-only server and loopback is always exempt.
	WriteRate           float64
	WriteBurst          int
	MaxConcurrentWrites int
}

RemoteRuntime is the resolved, ready-to-use remote configuration the server consumes. Unlike config.RemoteServerConfig it carries the already-resolved bearer token and concrete certificate paths, not the file/env indirections.

type ResponseEnvelope

type ResponseEnvelope struct {
	Data     any            `json:"data"`
	Curation CurationStatus `json:"curation"`
	// StoreReadonly is emitted (true) only when the store is in
	// store-level read-only mode, on every response -- reads included
	// -- so agents learn the store is frozen from any call. Omitted
	// on writable stores.
	StoreReadonly bool         `json:"store_readonly,omitempty"`
	Meta          ResponseMeta `json:"meta"`
}

ResponseEnvelope is the standard response wrapper.

type ResponseMeta

type ResponseMeta struct {
	DurationMs int64  `json:"duration_ms,omitempty"`
	Version    string `json:"version"`
}

ResponseMeta contains response metadata.

type Server

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

Server is the Gramaton HTTP daemon.

func New

func New(engine *core.Engine, cfg Config, logger *slog.Logger) (*Server, error)

New creates a server. An engine without a configured LLM provider is fine: autonomous curation (classification, summarization, contradiction detection, concept synthesis) silently skips when nil and the store falls back to the deterministic-only curation pipeline (lifecycle transitions, orphan linking, duplicate consolidation, concept candidate detection). See CLAUDE.md's "without LLM, curation runs in deterministic-only mode" contract. Downstream code already guards every LLM() use with a nil check; this function imposes no additional requirement.

Historical note: prior to the wizard shipping, server.New refused to construct without LLM. That constraint contradicted the documented deterministic-only contract and made the wizard's Skip-LLM option silently lead to a broken `gramaton serve` (CLI swallowed the server's startup error behind its 10s timeout). Removed.

func (*Server) Handler

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

Handler returns the HTTP handler for use with httptest.NewServer or other test infrastructure.

func (*Server) Log

func (s *Server) Log() *slog.Logger

Log returns the server's structured logger.

func (*Server) MCPHandler

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

MCPHandler returns an HTTP handler that serves the MCP protocol via Streamable HTTP transport.

Two servers are built once. Loopback callers (and authenticated remotes on an admin_ops server) get the full surface; other authenticated remotes get a trimmed surface with the path-taking tools removed, so a remote agent -- or a hijacked one -- cannot drive host-path operations even though it authenticated. The per-request selector picks between them; the global auth middleware has already rejected unauthenticated remotes before the request reaches here.

func (*Server) MCPServer

func (s *Server) MCPServer() *mcp.Server

MCPServer returns a configured MCP server for use with stdio transport.

func (*Server) RequestShutdown

func (s *Server) RequestShutdown()

RequestShutdown triggers a graceful shutdown from an API call. Caller is responsible for any response flushing -- this function only queues a shutdown reason on s.shutdownCh, which the main loop selects on. Portable across Unix and Windows (the earlier syscall.SIGTERM-to-self approach didn't work on Windows because Go only implements os.Kill for self-signaling there).

Non-blocking: if a shutdown is already pending, this request is dropped. First-reason-wins.

func (*Server) Run

func (s *Server) Run() error

Run starts the server and blocks until shutdown. It handles signals (SIGTERM, SIGINT), idle timeout, and writes the server info file for CLI discovery.

func (*Server) Shutdown

func (s *Server) Shutdown()

Shutdown gracefully stops the HTTP server, access flusher, prepared- sessions sweeper, and curation runner.

func (*Server) StartHTTP

func (s *Server) StartHTTP() error

StartHTTP starts the HTTP server and curation runner in background goroutines without blocking. Use Shutdown to stop. Designed for the MCP stdio process which needs the HTTP server running alongside the stdio transport.

type ServerInfo

type ServerInfo struct {
	PID       int       `json:"pid"`
	Port      int       `json:"port"`
	Bind      string    `json:"bind"`
	StartedAt time.Time `json:"started_at"`
	StoreDir  string    `json:"store_dir"`
	StoreName string    `json:"store_name,omitempty"`
	Version   string    `json:"version"`
}

ServerInfo is written to server.json for CLI discovery.

func ReadServerInfo

func ReadServerInfo(cfgDir string) (*ServerInfo, error)

ReadServerInfo reads the server info file from a config directory. Returns the info and nil error if the file exists and is valid.

Jump to

Keyboard shortcuts

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