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
- func IsProcessAlive(pid int) bool
- func MCPRemoteExcludedToolNames() []string
- func MCPToolAccess(name string) (access string, ok bool)
- func MCPWriteToolNames() []string
- func RegisterMCPProxy(cfgDir string) error
- func RemoveMCPProxy(cfgDir string, pid int)
- func RemoveServerInfo(cfgDir string)
- type Config
- type CurationStatus
- type ErrorDetail
- type ErrorResponse
- type MCPProxyInfo
- type RemoteRuntime
- type ResponseEnvelope
- type ResponseMeta
- type Server
- type ServerInfo
Constants ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
Handler returns the HTTP handler for use with httptest.NewServer or other test infrastructure.
func (*Server) MCPHandler ¶
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) 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 ¶
Run starts the server and blocks until shutdown. It handles signals (SIGTERM, SIGINT), idle timeout, and writes the server info file for CLI discovery.
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.
Source Files
¶
- admission.go
- api_bridge.go
- bindings_admin.go
- bindings_collections.go
- bindings_guide.go
- bindings_history.go
- bindings_maintenance.go
- bindings_records.go
- bindings_search.go
- bindings_sessions.go
- handler_intake.go
- handler_ops.go
- handler_records.go
- handler_search.go
- handlers.go
- info.go
- info_unix.go
- mcp.go
- mcp_intake.go
- mcp_readonly.go
- mcp_registry.go
- remote.go
- server.go
- service_error.go
- service_records.go
- validation.go