server

package
v0.116.3 Latest Latest
Warning

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

Go to latest
Published: Jul 9, 2026 License: MIT Imports: 59 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrNoPendingChunkedSync = errors.New("no pending chunked upload for op_id")

ErrNoPendingChunkedSync sentinel used by orchestrator helpers when a /chunked/{op_id} call returns 404. Exported via type assertion on the dispatch error path so the source can distinguish "target lost our pending entry" from a generic HTTP failure.

Functions

This section is empty.

Types

type Config

type Config struct {
	Addr           string
	DevMode        bool
	Logger         *slog.Logger
	StaticFS       fs.FS // embedded web/dist files for production
	Version        string
	NotifyManager  *notify.Manager
	AgentManager   *agent.Manager
	GroupDMManager *agent.GroupDMManager
	// BlobStore is optional; when nil, /api/v1/blob/... routes are not
	// registered. Phase 3 wires it in cmd/kojo/main.go.
	BlobStore *blob.Store
	// MaxBlobPutBytes overrides the per-PUT body cap. 0 = use the
	// package default (256MB). Tests pass a small cap so they can
	// exercise the 413 path without allocating hundreds of megabytes.
	MaxBlobPutBytes int64
	// EventBus is the invalidation broadcaster. When non-nil, the server
	// registers `GET /api/v1/events` (WebSocket) and exposes
	// Server.PublishEvent for write handlers. When nil, both the route
	// and PublishEvent become no-ops.
	EventBus *eventbus.Bus
	// Store is the kv-backed handle the session.Manager uses for
	// runtime persistence (Phase 2c-2 slice 28). Pass nil to disable
	// session persistence — tests that exercise non-session routes
	// can leave it nil; the production wiring in cmd/kojo/main.go
	// always passes the AgentManager's *store.Store handle.
	Store *store.Store
	// PeerIdentity is the local peer's stable {device_id, pubkey,
	// privkey} (Phase G). When non-nil the server registers
	// /api/v1/peers and uses DeviceID to flag the "self" row in list
	// responses + reject self-deletion. Nil disables the entire peers
	// API (test wiring + builds where peer identity load failed).
	PeerIdentity *peer.Identity
	// RequireIfMatch enables the docs §3.5 transition: every write
	// path that supports optimistic concurrency refuses requests
	// without an If-Match header (428 Precondition Required) instead
	// of allowing the legacy "best-effort" pass-through. Off by
	// default — operators flip it once their clients have all caught
	// up. cmd/kojo/main.go reads $KOJO_REQUIRE_IF_MATCH=1 to set this.
	RequireIfMatch bool
	// PeerEvents is the in-memory pub/sub for peer_registry
	// status mutations (docs §3.10). When non-nil the server
	// registers /api/v1/peers/events and the registrar /
	// OfflineSweeper publish to it on every status change.
	// Nil disables the cross-peer push surface.
	PeerEvents *peer.EventBus
	// PeerPresence is the in-memory active-connection set the events
	// WS handler maintains and the OfflineSweeper consults. Optional;
	// when nil the handler still touches last_seen periodically (so
	// the sample-based aging path keeps working) but the sweeper has
	// no way to skip an active connection on a flaky uplink.
	PeerPresence *peer.Presence
	// PeerOnly switches the server into the daemon-mode shape used
	// by `kojo --peer`. The §3.7 device-switch slice promoted this
	// from "minimal peer surface" to a full peer: agent runtime,
	// agent-facing routes, sessions, files, git, the inter-peer
	// surface — all registered — that's how a switch
	// can land the agent CLI on this host. What stays Hub-only
	// and 404s on a peer:
	//   - peer-registry mutation (POST/PATCH/DELETE on
	//     /api/v1/peers): pairing is an operator workflow, not
	//     something an agent or peer drives.
	//   - static SPA / dev proxy: peer hosts have no Web UI.
	// The auth middleware chain stays the same — RolePeer requests
	// arriving over tsnet get stamped by TailnetIdentityMiddleware
	// (peer_registry hit) via ServeAuthTsnet, and the agent
	// loopback listener keeps its Bearer-based AuthMiddleware.
	PeerOnly bool

	// V0LegacyDir, when non-empty, enables the session store's v0
	// fallback: on kv miss + no v1-dir sessions.json, Load() reads
	// <V0LegacyDir>/sessions.json and mirrors it into kv so the v0
	// → v1 cutover can reattach live tmux panes. The v0 file is
	// never unlinked from this path (rollback + `kojo --clean v0`
	// territory). Empty disables v0 fallback — supply "" on --fresh
	// / pure new-install paths where v0 data must not leak in.
	// cmd/kojo/main.go fills this from configdir.V0Path() iff the
	// startup gate observed v1Complete=true (migration done).
	V0LegacyDir string
	// PendingSyncKEK is the 32-byte envelope key used to seal
	// per-op state in pendingAgentSyncs into kv so the raw
	// $KOJO_AGENT_TOKEN survives a daemon restart between
	// agent-sync and finalize. main.go threads in the same KEK
	// it loaded via secretcrypto.LoadOrCreateKEK for the peer
	// identity row. Nil disables persistence (in-memory map
	// only); a restart with a pending op forces the
	// orchestrator to re-run the whole switch.
	PendingSyncKEK []byte
	// Unsafe disables Tailscale identity verification. Every caller
	// is admitted as RolePeer on a peer daemon, or as RoleOwner on
	// the public Hub listener. Intended for LAN dev / docker / CI
	// deployments. cmd/kojo gates this on the --unsafe flag and
	// refuses to start when --unsafe is OFF but no NodeKeyResolver
	// is available (i.e. the operator forgot to opt in).
	Unsafe bool

	// RepoDir is the source checkout POST /api/v1/system/rebuild runs
	// `make build` in, then copies the built binary over
	// os.Executable() (in-place deploy). Empty disables the endpoint
	// (returns 409). cmd/kojo reads $KOJO_REPO_DIR to set this.
	RepoDir string
}

type Server

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

func New

func New(cfg Config) *Server

func (*Server) Handler

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

func (*Server) PublishEvent added in v0.101.0

func (s *Server) PublishEvent(ev eventbus.Event)

PublishEvent is the Server-side hook for write handlers. Calls into the bus when configured, no-op otherwise. Always returns nil to keep call sites trivial — the bus reports its own internal errors via metrics (Bus.Dropped).

func (*Server) Serve

func (s *Server) Serve(ln net.Listener) error

func (*Server) ServeAuth added in v0.17.0

func (s *Server) ServeAuth(ln net.Listener, resolver *auth.Resolver) error

ServeAuth serves the agent-facing auth-required listener using the supplied resolver. The handler chain is AuthMiddleware (sets Principal from Authorization header) → EnforceMiddleware (denies non-Owner principals on routes outside the allowlist) → mux.

Intended use: bind to 127.0.0.1 only, expose to local PTY processes via $KOJO_API_BASE. For a tsnet listener that ALSO needs to accept inter-peer traffic (peer-mode primary listener), use ServeAuthTsnet instead — it wires TailnetIdentityMiddleware ahead of AuthMiddleware so a paired peer's WhoIs-resolved identity reaches the policy gate as RolePeer / RoleOwner. The public listener (Serve) bypasses auth and keeps the user UX intact.

func (*Server) ServeAuthTsnet added in v0.101.0

func (s *Server) ServeAuthTsnet(ln net.Listener, resolver *auth.Resolver) error

ServeAuthTsnet serves the auth-required listener on a tsnet listener that also receives inter-peer traffic (peer-mode primary listener). Chain order: TailnetIdentityMiddleware → AuthMiddleware → EnforceMiddleware → … → mux. PromoteUnknownTailnetToOwner is false — only registered peers get a non-Guest principal via WhoIs; unknown tailnet callers fall through to AuthMiddleware so an agent on the same tailnet (rare) can still present a Bearer. The legacy Ed25519-signing path is retired; Tailnet identity (peer_registry membership over a tsnet listener) is the sole inter-peer trust signal here. The --unsafe escape hatch stamps RoleOwner via UnsafeAsHub=true for LAN / docker / CI deployments — peer↔peer handlers gate on the source_device_id carried in the request body, so Owner skips the equality gates rather than failing on an empty PeerID.

func (*Server) ServeTLS

func (s *Server) ServeTLS(ln net.Listener, certFile, keyFile string) error

func (*Server) SetNodeKeyResolver added in v0.101.0

func (s *Server) SetNodeKeyResolver(fn func(ctx context.Context, remoteAddr string) (string, error))

SetNodeKeyResolver wires (or rewires) the tsnet WhoIs resolver. cmd/kojo calls this AFTER tsnet.Server.LocalClient is available (Hub) or once the OS tailscaled handshake completes (--peer). Passing nil clears the resolver — every caller becomes Guest unless Unsafe is set, which is the safe default on --local / --dev where no tsnet boundary exists.

func (*Server) SetOnAgentForceReclaimed added in v0.101.0

func (s *Server) SetOnAgentForceReclaimed(fn func(ctx context.Context, agentID string))

SetOnAgentForceReclaimed installs the operator-driven force- reclaim hook. See onAgentForceReclaimed for the contract.

func (*Server) SetOnAgentReleasedAsSource added in v0.101.0

func (s *Server) SetOnAgentReleasedAsSource(fn func(ctx context.Context, agentID string))

onAgentReleasedAsSource is fired by the §3.7 switch_device handler after a successful complete + finalize. The hook drops the agent from this peer's AgentLockGuard.desired set so target's lease expiry doesn't trigger a re-Acquire from this peer (which would steal the lock back with stale state). Set via SetOnAgentReleasedAsSource.

func (*Server) SetOnAgentRuntimePurged added in v0.101.0

func (s *Server) SetOnAgentRuntimePurged(fn func(ctx context.Context, agentID string))

SetOnAgentRuntimePurged installs the inter-peer state-probe self-heal hook. See onAgentRuntimePurged for the contract.

func (*Server) SetOnAgentSyncFinalized added in v0.101.0

func (s *Server) SetOnAgentSyncFinalized(fn func(ctx context.Context, agentID, rawToken, sourceDeviceID, opID string) (bool, error))

SetOnAgentSyncFinalized installs the post-complete hook that the orchestrator's /api/v1/peers/agent-sync/finalize endpoint invokes. See onAgentSyncFinalized for the contract.

func (*Server) SetOnAgentSynced added in v0.101.0

func (s *Server) SetOnAgentSynced(fn func(ctx context.Context, agentID string) error)

SetOnAgentSynced installs the post-agent-sync hook that handlePeerAgentSync invokes after a successful row write. The hook MUST be set on the target peer before it can receive a §3.7 device-switch agent-sync payload — without it, the synced rows land in kojo.db but no one tells the agent_lock guard or the in-memory agent.Manager that a new agent has arrived.

Threadsafe wrt the handler: handlePeerAgentSync reads the field after the Server's mux has been built, by which point New() has long returned and any cmd/kojo/main.go wiring is in place. We don't bother with a mutex — the field is set exactly once at boot and never mutated under load.

func (*Server) SetRestartTrigger added in v0.111.0

func (s *Server) SetRestartTrigger(fn func() bool)

SetRestartTrigger wires the shutdown-for-restart callback. cmd/kojo passes a closure that marks the restart intent and cancels the signal context, funneling into the same ordered graceful-shutdown path as SIGINT; after that drain main re-execs the binary in place. The closure reports whether the intent was accepted (false = a signal shutdown already won the race). When never set (tests, unsupported platforms), the handler returns 501.

func (*Server) SetSelfNodeKey added in v0.101.0

func (s *Server) SetSelfNodeKey(nk string)

SetSelfNodeKey records the local kojo's own NodeKey so the tsnet identity middleware can promote requests from the same host to RoleOwner. Same wiring contract as SetNodeKeyResolver.

func (*Server) SetTLSConfig

func (s *Server) SetTLSConfig(tlsCfg *tls.Config)

func (*Server) Shutdown

func (s *Server) Shutdown(ctx context.Context) error

func (*Server) SlackHub added in v0.101.0

func (s *Server) SlackHub() *slackbot.Hub

SlackHub returns the wired-in slackbot.Hub or nil when the server was built without one (--peer mode or test fixtures). Used by cmd/kojo/main.go's source-release hook to stop a migrated agent's Slack bot on the source peer.

func (*Server) StartIdempotencySweep added in v0.101.0

func (s *Server) StartIdempotencySweep(ctx context.Context)

StartIdempotencySweep launches a goroutine that periodically deletes expired idempotency_keys rows. Cancelling ctx stops the goroutine. Safe to call from multiple sites — the second call is a no-op.

type WSAttachmentMsg added in v0.5.0

type WSAttachmentMsg struct {
	Type        string                `json:"type"`
	Attachments []*session.Attachment `json:"attachments"`
}

type WSExitMsg

type WSExitMsg struct {
	Type     string `json:"type"`
	ExitCode int    `json:"exitCode"`
	Live     bool   `json:"live"`
}

type WSInputMsg

type WSInputMsg struct {
	Type string `json:"type"`
	Data string `json:"data"` // base64
}

type WSMessage

type WSMessage struct {
	Type string          `json:"type"`
	Data json.RawMessage `json:"data,omitempty"`
}

WebSocket message types

type WSOutputMsg

type WSOutputMsg struct {
	Type string `json:"type"`
	Data string `json:"data"` // base64
}

type WSResizeMsg

type WSResizeMsg struct {
	Type string `json:"type"`
	Cols int    `json:"cols"`
	Rows int    `json:"rows"`
}

type WSScrollbackMsg

type WSScrollbackMsg struct {
	Type string `json:"type"`
	Data string `json:"data"` // base64
}

type WSYoloDebugMsg

type WSYoloDebugMsg struct {
	Type string `json:"type"`
	Tail string `json:"tail"`
}

Jump to

Keyboard shortcuts

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