bot

package
v1.801.413 Latest Latest
Warning

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

Go to latest
Published: Aug 4, 2026 License: Apache-2.0 Imports: 29 Imported by: 0

Documentation

Overview

Package bot is your own machines, connected and ready to take a command.

The node control plane at /v1/bot: bot nodes on user machines dial in and hold a socket, and an org lists its connected nodes and invokes commands on one, authorized once at the socket.

GET  /v1/bot/connect            the socket a node dials and holds open
GET  /v1/bot/nodes              this org's connected nodes
POST /v1/bot/nodes/{id}/invoke  ask one of them to run a command
POST /v1/bot/peer/invoke        replica-to-replica forward (machine hop)

The org is the gateway's verdict, never the caller's

Every route here takes its org from X-Org-Id, which the gateway injects after validating IAM and after stripping any client copy. It is never read from a body, a query, or a path — a caller that could name an org could name someone else's. principal.Org returns nothing without a validated principal, so the direct-to-pod path (where a forged X-Org-Id is restored but no user is) is refused rather than defaulted.

The one exception is the peer hop, whose org DOES arrive in a body: it is a machine call from another replica of this binary, which derived that org from a validated header, and it authenticates with a shared token instead of a user. See PeerHandler.

Where authorization happens

Once, at the socket. The registry runs the gate on the replica that holds the node — the only one that knows what that node declared it can do — so a locally-held node and one reached through a forward are authorized by the same code with the same session in hand. This file supplies that gate (policy.Check over the deployment's Mode); it does not re-check before invoking, because a second check in a place that sometimes has the session and sometimes does not is how the two answers drift apart.

Index

Constants

View Source
const (
	CodeNoSession              = "NO_SESSION"
	CodeCommandRequired        = "COMMAND_REQUIRED"
	CodeCommandNotAllowlisted  = "COMMAND_NOT_ALLOWLISTED"
	CodeCommandsNotDeclared    = "COMMANDS_NOT_DECLARED"
	CodeCommandNotDeclared     = "COMMAND_NOT_DECLARED"
	CodeExecApprovalsForbidden = "EXEC_APPROVALS_FORBIDDEN"
	CodeAuthModeForbids        = "AUTH_MODE_FORBIDS_COMMAND"
	CodeArgvRequired           = "ARGV_REQUIRED"

	CodeMissingCommand     = "MISSING_COMMAND"
	CodeRawCommandMismatch = "RAW_COMMAND_MISMATCH"
	CodeMissingRunID       = "MISSING_RUN_ID"
	CodeApprovalsUnavail   = "APPROVALS_UNAVAILABLE"
	CodeUnknownApprovalID  = "UNKNOWN_APPROVAL_ID"
	CodeApprovalExpired    = "APPROVAL_EXPIRED"
	CodeMissingNodeID      = "MISSING_NODE_ID"
	CodeNodeBindingMissing = "APPROVAL_NODE_BINDING_MISSING"
	CodeNodeMismatch       = "APPROVAL_NODE_MISMATCH"
	CodeDeviceMismatch     = "APPROVAL_DEVICE_MISMATCH"
	CodeClientMismatch     = "APPROVAL_CLIENT_MISMATCH"
	CodeRequestMismatch    = "APPROVAL_REQUEST_MISMATCH"
	CodeEnvBindingMissing  = "APPROVAL_ENV_BINDING_MISSING"
	CodeEnvMismatch        = "APPROVAL_ENV_MISMATCH"
	CodeApprovalRequired   = "APPROVAL_REQUIRED"
)

Decision codes. The approval codes match the TypeScript gateway's details.code values so an existing node/CLI keeps recognizing them.

View Source
const (
	CommandSystemRun        = "system.run"
	CommandSystemRunPrepare = "system.run.prepare"
	CommandSystemWhich      = "system.which"
	CommandSystemNotify     = "system.notify"
	CommandBrowserProxy     = "browser.proxy"
)

Commands the gateway itself knows by name.

View Source
const (
	RateLimitScopeDefault      = "default"
	RateLimitScopeSharedSecret = "shared-secret"
	RateLimitScopeDeviceToken  = "device-token"
	RateLimitScopeHookAuth     = "hook-auth"
)

Rate-limit scopes keep credential classes on independent budgets, so failing device-token auth cannot lock out shared-secret auth.

View Source
const (
	ScopeOperatorAdmin     = "operator.admin"
	ScopeOperatorApprovals = "operator.approvals"
)

Scopes that let a caller speak for the approval system itself.

View Source
const ApprovalHostNode = "node"

ApprovalHostNode is the only host an approval may bind to for a node invoke.

View Source
const (
	DefaultPresenceTTL = 45 * time.Second
)

Defaults for the presence lease. TTL is the window in which a replica that dies without releasing still advertises its sockets; renew at a third of it so two consecutive KV failures do not expire a live claim.

View Source
const (
	// PeerInvokePath is where PeerHandler must be mounted for NewHTTPHop to find
	// it. One constant, both ends.
	PeerInvokePath = "/v1/bot/peer/invoke"
)

The peer hop's wire. Both ends are this file, in this binary.

Variables

View Source
var (
	// ErrNoSuchNode means no node with that id is connected FOR THAT ORG. It is
	// deliberately indistinguishable from "exists but belongs to another org":
	// telling those apart would leak the existence of another tenant's nodes.
	ErrNoSuchNode = errors.New("bot: no such node")

	// ErrInvokeTimeout means the node held the socket but did not answer.
	ErrInvokeTimeout = errors.New("bot: node invoke timed out")

	// ErrNodeGone means the socket closed while the call was in flight.
	ErrNodeGone = errors.New("bot: node disconnected mid-invoke")
)

Errors a caller is expected to handle.

View Source
var ErrAmbiguousAuthMode = errors.New(
	"invalid config: gateway.auth.token and gateway.auth.password are both configured, " +
		"but gateway.auth.mode is unset; set gateway.auth.mode to token or password")

ErrAmbiguousAuthMode is returned when two shared secrets are configured and nothing says which one authenticates.

Functions

func Allowlist

func Allowlist(p Platform, mode Mode) []string

Allowlist resolves the commands reachable on a platform under this mode, sorted. Deny is applied last, so a denied command is denied however it got in.

func CanonicalClientIP

func CanonicalClientIP(ip string) string

CanonicalClientIP reduces a client address to one representation, so that 1.2.3.4 and ::ffff:1.2.3.4 spend the same budget. Anything unparseable becomes "unknown" and shares one budget, which is the conservative choice.

func CheckAuthMode

func CheckAuthMode(mode AuthMode, hasToken, hasPassword bool) error

CheckAuthMode refuses a configuration that does not say how callers authenticate.

The TypeScript this ports had drifted to always returning "not ambiguous", on the reasoning that a token is the only shared secret left; its own tests still assert the opposite. Taking the safer reading: two configured secrets with no declared mode is a config whose author has not decided, and guessing on their behalf picks a credential nobody meant to be live.

func DangerousCommands

func DangerousCommands() []string

DangerousCommands lists the high-risk commands, off by default everywhere.

func FormatExecCommand

func FormatExecCommand(argv []string) string

FormatExecCommand renders an argv the way an approval prompt shows it.

func InvokeFrame

func InvokeFrame(key NodeKey, command string, params json.RawMessage, timeout time.Duration, idempotencyKey string) func(corrID string) ([]byte, error)

InvokeFrame builds the node.invoke.request event for one call, ready to hand to Registry.Invoke — which mints the correlation id and passes it in.

It takes a NodeKey rather than a bare id so a frame cannot be built without having named a tenant, even though only the node id goes on the wire: which org a node belongs to is the gateway's business, never the node's.

params is the command's arguments as JSON, nil for none.

func IsDangerous

func IsDangerous(command string) bool

IsDangerous reports whether a command is high risk.

func IsHostExec

func IsHostExec(command string) bool

IsHostExec reports whether a command runs a program on the node's machine.

func Mount

func Mount(app cloud.Router, deps cloud.Deps) error

Mount registers the /v1/bot surface per HIP-0106.

func NodeWS

func NodeWS(reg *Registry, opts WSOptions) zip.Handler

NodeWS returns the handler nodes dial. Mount it on a GET route; reg is where the sessions it opens become addressable.

func PlatformCommands

func PlatformCommands(p Platform) []string

PlatformCommands returns a platform's defaults, sorted.

func RequiresTokenForInstall

func RequiresTokenForInstall(mode AuthMode) bool

RequiresTokenForInstall reports whether the node install flow must present a gateway token. Only the two modes that explicitly move authentication elsewhere are exempt; an unset or unrecognized mode requires the token.

func SanitizeSystemRun

func SanitizeSystemRun(key NodeKey, in SystemRunParams, caller Caller, store ApprovalStore, now time.Time) (SystemRunParams, Decision)

SanitizeSystemRun decides what a system.run invocation may forward.

It gates the approval control fields behind a real approval record, so a caller holding only write access cannot approve their own command by setting approved=true. On denial the returned params are zero: there is nothing safe to forward.

func Shutdown

func Shutdown(ctx context.Context) error

Shutdown ends the presence-renew loop, which releases this replica's claims on the way out so peers stop forwarding into a pod that is draining.

Types

type ApprovalBinding

type ApprovalBinding struct {
	Argv       []string
	Cwd        string
	AgentID    string
	SessionKey string
	EnvHash    string
}

ApprovalBinding is what the operator actually said yes to.

func BuildApprovalBinding

func BuildApprovalBinding(argv []string, cwd, agentID, sessionKey string, env map[string]string) (ApprovalBinding, []string)

BuildApprovalBinding derives what an approval binds to. The returned keys are the env variable names, safe to show a human in an approval prompt; the values are only ever hashed.

type ApprovalDecision

type ApprovalDecision string

ApprovalDecision is what an operator answered.

const (
	DecisionAllowOnce   ApprovalDecision = "allow-once"
	DecisionAllowAlways ApprovalDecision = "allow-always"
)

type ApprovalPlan

type ApprovalPlan struct {
	Argv       []string
	Cwd        string
	RawCommand string
	AgentID    string
	SessionKey string
}

ApprovalPlan is the command the gateway resolved when it asked the operator. When a record carries one, it — not the caller's parameters — is what runs.

type ApprovalRecord

type ApprovalRecord struct {
	RunID   string
	Node    NodeKey
	Host    string
	Binding *ApprovalBinding
	Plan    *ApprovalPlan

	ExpiresAt time.Time

	RequestedByConnID   string
	RequestedByDeviceID string

	// Decision is empty once consumed or when the request timed out.
	Decision ApprovalDecision
	// Resolved records that the request reached an end state.
	Resolved bool
	// ResolvedBy is empty when nobody answered, i.e. the request timed out.
	ResolvedBy string
}

ApprovalRecord is the gateway's record of one approval request.

Node is a NodeKey, not a bare id: an approval is an answer about one tenant's machine. Binding by id alone would let an approval for org A's "laptop" authorize org B's "laptop".

type ApprovalStore

type ApprovalStore interface {
	// Snapshot returns the record for runID, or nil if unknown or expired.
	Snapshot(runID string) *ApprovalRecord
	// ConsumeAllowOnce spends a one-shot approval, returning false if it was
	// already spent. Replay protection lives here because it must be atomic.
	ConsumeAllowOnce(runID string) bool
}

ApprovalStore is the gateway's live approval state. It is an interface so this file stays pure: the only mutation policy needs is consuming a one-shot approval, and that has to be atomic with the check.

type AuthMode

type AuthMode string

AuthMode is how the edge authenticates a caller.

const (
	// AuthUnset is the zero value and is treated as the strictest reading.
	AuthUnset        AuthMode = ""
	AuthIAM          AuthMode = "iam"
	AuthToken        AuthMode = "token"
	AuthTrustedProxy AuthMode = "trusted-proxy"
	// AuthNone means the edge authenticates nobody.
	AuthNone AuthMode = "none"
)

type Caller

type Caller struct {
	ConnID   string
	DeviceID string
	Scopes   []string
}

Caller is who asked for the invocation.

type Cluster

type Cluster struct {
	// Replica is this replica's stable id (CLOUD_POD_NAME / the StatefulSet
	// ordinal). It is what a presence claim names and what a Hop resolves.
	Replica string

	Presence PresenceStore
	Hop      Hop

	// TTL is the presence lease; Renew defaults to a third of it.
	TTL   time.Duration
	Renew time.Duration

	// PeerToken authenticates a forward. Empty disables PeerHandler entirely —
	// see PeerHandler for why that is the only safe default.
	PeerToken string

	Logger luxlog.Logger
}

Cluster is what one replica needs to reach sockets it does not hold.

type Decision

type Decision struct {
	Allow bool
	// Code is stable and safe to put on the wire. Callers switch on it.
	Code string
	// Reason is for a human reading a log or an error body.
	Reason string
}

Decision is the answer this package gives. The zero value denies: a policy that has not run cannot have said yes.

func Check

func Check(s *Session, command string, args []string, mode Mode) Decision

Check decides whether one command may reach one node.

args is the invocation's argv, and is consulted only for the host-exec commands where the argv IS the command being run; for everything else it is ignored. A caller that does not pass it for system.run gets a denial, which is the failure direction we want.

Session.Permissions is deliberately not consulted: it is the node's report of its own OS grants, useful to show a human, and not something the node's own operator can be prevented from lying about. The gate is the allowlist, the declared commands, and the approval record.

func EvaluateApprovalMatch

func EvaluateApprovalMatch(argv []string, rec *ApprovalRecord, cwd, agentID, sessionKey string, env map[string]string) Decision

EvaluateApprovalMatch reports whether a record answers the question actually being asked: same argv, same cwd, same agent, same session, same env.

A record with no binding never matches. An approval that predates binding is an approval to an unknown question.

func ResolveSystemRunCommand

func ResolveSystemRunCommand(command []string, rawCommand string) (argv []string, cmdText string, d Decision)

ResolveSystemRunCommand validates a command line and returns the argv to bind against, plus the text a human would be shown.

rawCommand is display text, and display text that disagrees with the argv is the whole attack: an operator approves `echo`, the machine runs `cmd.exe /d /s /c echo SAFE&&whoami`.

type Denied

type Denied struct {
	Code    string
	Message string
}

Denied is a gate refusal. It carries a stable code so the caller can answer with it, and it survives a peer forward.

func (*Denied) Error

func (d *Denied) Error() string

type Frame

type Frame func(corrID string) ([]byte, error)

Frame builds the wire frame for one invocation.

corrID is minted by the replica that HOLDS the socket, and only by it: that replica's pending table is keyed by it, its disconnect sweep cancels by its prefix, and the node echoes it back on a socket where anything else is refused. An EMPTY corrID means "not minted yet" — the frame built with one is the frame that travels to the owning replica, which stamps its own id in before writing it.

type Gate

type Gate func(s *Session, command string, params json.RawMessage) error

Gate authorizes one invocation, on the replica that holds the node's socket — the only replica that knows what that node declared it can do.

It is called at the SINGLE point where a frame is about to be written to a socket, so no path into a node can skip it: a locally-held node and one reached through a peer forward pass the same gate, once, with the same session in hand. Returning a *Denied makes the refusal travel a hop intact, so a caller gets the same answer wherever the socket happens to be.

A nil gate authorizes everything. That is what a bare plumbing registry wants (fixtures, the routing tests) and what a serving one must never have.

type Hop

type Hop interface {
	Forward(ctx context.Context, replica string, key NodeKey, req []byte, timeout time.Duration) (InvokeResult, error)
}

Hop carries an invocation to the replica that holds the node's socket.

WHY HTTP AND NOT ZAP. ZAP is cloud's answer for INTER-SERVICE RPC — one service calling another across a trust boundary. This is not that: it is one replica of cloud reaching another replica of the SAME binary because a socket landed there, and cloud has already answered that question exactly once (shardrouter forwards an org's request to the pod owning its files, over the peer address from CLOUD_PEERS). A second mechanism for "reach my peer pod" would be a second answer to a settled question. Nor is there a ZAP client seam to reuse: zapface is a server face whose dispatch replays each call as an in-process HTTP request into the same Fiber app, so a ZAP hop here would be this HTTP hop with an extra codec in front of it.

WHY NOT THE KV'S OWN PUB/SUB (publish to bot:invoke:<pod>, listen on bot:invoke-result:<originPod>, which is how this worked before): a request/response spliced out of two fire-and-forget channels needs a second correlation table, with its own timeouts and its own cleanup, layered on the one the registry already keeps — and it cannot tell a dead owner from a slow one, so a replica that died between the lookup and the publish costs the caller a full timeout instead of a dial error. One synchronous connection makes the correlation the connection itself.

The interface is here so that transport can be replaced without the routing above it noticing.

func NewHTTPHop

func NewHTTPHop(resolve func(replica string) (addr string, ok bool), token string) Hop

NewHTTPHop forwards over the peer address the deployment already knows.

resolve turns a replica id into its address — the same id→addr mapping the shard router elects over (CLOUD_PEERS, or the live membership), passed as a function so the registry depends on the shape of that answer and not on where it comes from. An address with no scheme is dialed as in-cluster http.

type InvokeResult

type InvokeResult struct {
	OK      bool
	Payload []byte
	Code    string
	Message string
}

InvokeResult is what a node answered.

type KV

type KV interface {
	Set(ctx context.Context, key string, value any, expiration time.Duration) *kv.StatusCmd
	Get(ctx context.Context, key string) *kv.StringCmd
	Eval(ctx context.Context, script string, keys []string, args ...any) *kv.Cmd
}

KV is the slice of the Hanzo KV client the presence map uses. Narrow on purpose: the presence map is three operations, and naming only those three keeps the whole KV surface out of the registry's reach. github.com/hanzokv/go/v9's Cmdable satisfies it, so *kv.Client and *kv.ClusterClient are passed as-is.

type Mode

type Mode struct {
	Auth AuthMode
	// Allow adds commands on top of the platform defaults. This is the only
	// way a dangerous command becomes reachable.
	Allow []string
	// Deny removes commands, and wins over Allow and over the defaults.
	Deny []string
}

Mode is the operator's policy for this deployment: the auth posture plus the explicit overlay on the platform defaults. Its zero value is the safe one — no extra commands, unknown auth treated as strict.

type NodeKey

type NodeKey struct {
	Org    string
	NodeID string
}

NodeKey addresses a node. Both fields are required; there is no way to name a node without naming its tenant.

func (NodeKey) String

func (k NodeKey) String() string

type Option

type Option func(*Registry)

Option is construction-time wiring. There is one constructor; what a Registry can do is what it was given, and nothing is switched on afterwards.

func WithCluster

func WithCluster(c Cluster) Option

WithCluster makes this registry reachable from its peers. Omitting it is the single-replica registry, unchanged.

func WithGate

func WithGate(g Gate) Option

WithGate installs the authorization every invocation passes at the socket.

type Platform

type Platform string

Platform is a node's classified operating system. It is derived from self-reported metadata, so it is a hint the policy hardens against, never a credential.

const (
	PlatformIOS     Platform = "ios"
	PlatformAndroid Platform = "android"
	PlatformMacOS   Platform = "macos"
	PlatformWindows Platform = "windows"
	PlatformLinux   Platform = "linux"
	PlatformUnknown Platform = "unknown"
)

func ClassifyPlatform

func ClassifyPlatform(platform, deviceFamily string) Platform

ClassifyPlatform resolves self-reported metadata to a platform.

Metadata is normalized (NFKD, marks stripped, lowercased) before matching so a confusable spelling cannot dodge classification: "ĺinux" must land on linux, not on the unknown bucket, because unknown grants canvas and camera that linux does not.

Session carries no device family, so Check classifies from Platform alone. A transport that has family metadata should resolve it here before registering the session.

type PresenceStore

type PresenceStore interface {
	// Claim takes ownership unconditionally. A node that reconnects to another
	// replica must win immediately — the old replica's socket is dead whether or
	// not it has noticed yet.
	Claim(ctx context.Context, key NodeKey, replica string, ttl time.Duration) error

	// Renew extends a claim this replica still owns, and re-takes one that has
	// gone missing (a claim lost to a KV blip heals on the next tick). It must
	// NOT overwrite another replica's claim: a socket that has already moved
	// would otherwise be dragged back to a replica holding a half-open TCP
	// connection, where every invoke burns its full timeout.
	Renew(ctx context.Context, key NodeKey, replica string, ttl time.Duration) error

	// Release drops a claim only while it is still ours, for the same reason.
	Release(ctx context.Context, key NodeKey, replica string) error

	// Lookup returns the replica holding the node, or ErrNoSuchNode.
	Lookup(ctx context.Context, key NodeKey) (string, error)
}

PresenceStore is the shared answer to "which replica holds this node's socket". Lookup reports ErrNoSuchNode for an absent claim: to a caller, a node nobody holds and a node in another tenant are the same non-answer, and that sameness is the point.

func NewKVPresence

func NewKVPresence(c KV) PresenceStore

NewKVPresence backs the presence map with Hanzo KV.

type RateLimitConfig

type RateLimitConfig struct {
	MaxAttempts int
	Window      time.Duration
	Lockout     time.Duration
	// RateLimitLoopback turns off the loopback exemption. It is phrased
	// negatively so the zero value keeps a local CLI from locking itself out.
	RateLimitLoopback bool
	// Now is the injected clock. nil means time.Now.
	Now func() time.Time
}

RateLimitConfig configures a RateLimiter. The zero value is the default policy: 10 attempts a minute, five minute lockout, loopback exempt.

type RateLimitResult

type RateLimitResult struct {
	Allow      bool
	Remaining  int
	RetryAfter time.Duration
}

RateLimitResult answers "may this client try again".

type RateLimiter

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

RateLimiter is a sliding-window limiter for failed authentication attempts, keyed by (scope, client ip).

The org is deliberately NOT part of that key. Everywhere else in this package the org is part of an identity; here the key is a budget, and a budget partitioned by something the caller names is no budget at all — an attacker would multiply their attempts by inventing org names.

func NewRateLimiter

func NewRateLimiter(cfg RateLimitConfig) *RateLimiter

NewRateLimiter builds a limiter. It owns no goroutine and no timer: call Prune from whatever the process already schedules.

func (*RateLimiter) Check

func (l *RateLimiter) Check(ip, scope string) RateLimitResult

Check reports whether ip may attempt authentication in scope.

func (*RateLimiter) Prune

func (l *RateLimiter) Prune()

Prune drops budgets that no longer hold anything. A locked-out entry is kept until its lockout expires, or pruning would release the lock.

func (*RateLimiter) RecordFailure

func (l *RateLimiter) RecordFailure(ip, scope string)

RecordFailure counts one failed attempt.

func (*RateLimiter) Reset

func (l *RateLimiter) Reset(ip, scope string)

Reset clears one (scope, ip) budget, e.g. after a successful login.

func (*RateLimiter) Size

func (l *RateLimiter) Size() int

Size is the number of tracked budgets.

type Registry

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

Registry tracks connected nodes, correlates in-flight invocations, and — when wired for a cluster — routes to sockets held by another replica.

It is transport-agnostic about the SOCKET on purpose: the WS layer registers a Session with a send func and feeds answers back by correlation id. Keeping the socket out of here is what lets the routing be tested without a network.

func NewRegistry

func NewRegistry(opts ...Option) *Registry

NewRegistry builds the one registry. With no options it is a single replica holding its own sockets — which is the whole of what a one-pod deployment needs, and the substrate every clustered path runs on.

func (*Registry) Answer

func (r *Registry) Answer(corrID string, res InvokeResult)

Answer delivers a node's reply to whoever is waiting on it.

An answer with no waiter is dropped rather than queued: it means the caller already timed out or went away, and holding it would only surface as a reply to an unrelated later call.

func (*Registry) Invoke

func (r *Registry) Invoke(ctx context.Context, key NodeKey, frame Frame, timeout time.Duration) (InvokeResult, error)

Invoke sends a command to a node, wherever in the cluster it is attached, and waits for its answer.

The org comes from the caller's validated identity, never from the request body, so a caller cannot reach another tenant's node by naming it.

The order — local first, presence second — keeps the single-replica path free of any KV round trip on the common case, and makes the clustered path the same code plus one lookup.

func (*Registry) List

func (r *Registry) List(org string) []*Session

List returns the nodes connected TO THIS REPLICA for one org, and only that org.

It is deliberately not cluster-wide: presence answers "which replica holds node X", which is what routing needs, and a fleet-wide roster is a different question (a scan of bot:node:<org>:*, exact because the org is its own key segment) with a different value shape.

func (*Registry) PeerHandler

func (r *Registry) PeerHandler() http.Handler

PeerHandler serves a forwarded invocation against THIS replica's sockets.

The one place an org arrives in a body

Everywhere else the org comes from the gateway-injected X-Org-Id, precisely so no caller can name another tenant's node. Here it arrives in the body, because the request has already crossed the identity boundary on the replica that forwarded it — it is a machine hop, not a user call. What makes that safe is the token: it proves the caller is another replica of this binary, which derived that org from a validated header. With no token configured the handler serves nothing at all, because an unauthenticated endpoint that takes an org from a body is a cross-tenant invoke primitive for anything that can reach the pod.

It invokes strictly LOCALLY, so a hop cannot chain into another hop: forwarding is loop-free by construction, with no hop counter to maintain. The gate runs here too, because invokeLocal is where it runs — a forwarded invocation is authorized by the replica that knows the node, exactly like a local one.

Mount it outside the user-identity middleware; on zip that is zip.AdaptNetHTTP.

func (*Registry) Register

func (r *Registry) Register(s *Session) error

Register adds a connected node, and claims it in the presence map.

A second connection for the same (org, node) replaces the first and closes nothing: the old socket is simply no longer addressable. A node that reconnects after a network blip would otherwise be unreachable behind a dead entry until a timeout expired.

A failed presence claim does NOT fail the registration. The node IS connected here and invocations landing here work; only invocations landing elsewhere miss — which they read as ErrNoSuchNode, the same answer as not-connected, so a KV outage degrades reachability without leaking anything. Refusing the registration instead would disconnect an entire fleet every time the KV blips. The renew loop re-takes the claim, so it heals within one tick.

func (*Registry) Run

func (r *Registry) Run(ctx context.Context)

Run keeps this replica's claims alive until ctx ends, then drops them.

The TTL is what unstrands nodes when a replica dies outright; releasing on a clean shutdown is what stops peers forwarding into a pod that is draining, during the window before that TTL would have expired. On a single-replica registry there is nothing to advertise, so it returns at once.

func (*Registry) Unregister

func (r *Registry) Unregister(connID string)

Unregister removes a node by its connection id, fails every call still waiting on it, and releases its presence claim.

A pending invoke whose node has gone will never be answered; leaving it to time out would hold the caller for the full timeout on a question that is already unanswerable.

The claim is released only if THIS connection is still the one holding the node. A node that reconnected here on a new socket, or moved to another replica, must not have its live claim released by the late teardown of the socket it left.

type Session

type Session struct {
	Key         NodeKey
	ConnID      string
	DisplayName string
	Platform    string
	Version     string
	Caps        []string
	Commands    []string
	Permissions map[string]bool
	RemoteIP    string
	ConnectedAt time.Time
	// contains filtered or unexported fields
}

Session is a connected node.

type SystemRunParams

type SystemRunParams struct {
	Command              []string          `json:"command,omitempty"`
	RawCommand           string            `json:"rawCommand,omitempty"`
	Cwd                  string            `json:"cwd,omitempty"`
	Env                  map[string]string `json:"env,omitempty"`
	TimeoutMs            int               `json:"timeoutMs,omitempty"`
	NeedsScreenRecording bool              `json:"needsScreenRecording,omitempty"`
	AgentID              string            `json:"agentId,omitempty"`
	SessionKey           string            `json:"sessionKey,omitempty"`
	RunID                string            `json:"runId,omitempty"`

	// Approved and ApprovalDecision are control fields. A caller may ask for
	// them; only a matching approval record puts them on the wire.
	Approved         bool             `json:"approved,omitempty"`
	ApprovalDecision ApprovalDecision `json:"approvalDecision,omitempty"`
}

SystemRunParams is exactly the set of fields a node's system.run handler understands.

The type is the allowlist. The TypeScript had to copy a hand-written list of keys out of an untyped object to stop internal control fields being smuggled through; here a field the node does not implement has nowhere to live.

type WSOptions

type WSOptions struct {
	// ServerVersion is reported to the node in hello-ok.
	ServerVersion string

	Logger luxlog.Logger

	// OnNodeEvent receives a node's unsolicited node.event pushes (idle
	// notices, proxy stream chunks). payload is raw JSON, already unwrapped
	// when the node sent it as payloadJSON. Nil drops them, which is the
	// correct default: an unread push is cheaper than a fabricated consumer.
	OnNodeEvent func(key NodeKey, event string, payload []byte)
}

WSOptions configures the node transport. Every field is optional.

Jump to

Keyboard shortcuts

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