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
- Variables
- func Allowlist(p Platform, mode Mode) []string
- func CanonicalClientIP(ip string) string
- func CheckAuthMode(mode AuthMode, hasToken, hasPassword bool) error
- func DangerousCommands() []string
- func FormatExecCommand(argv []string) string
- func InvokeFrame(key NodeKey, command string, params json.RawMessage, timeout time.Duration, ...) func(corrID string) ([]byte, error)
- func IsDangerous(command string) bool
- func IsHostExec(command string) bool
- func Mount(app cloud.Router, deps cloud.Deps) error
- func NodeWS(reg *Registry, opts WSOptions) zip.Handler
- func PlatformCommands(p Platform) []string
- func RequiresTokenForInstall(mode AuthMode) bool
- func SanitizeSystemRun(key NodeKey, in SystemRunParams, caller Caller, store ApprovalStore, ...) (SystemRunParams, Decision)
- func Shutdown(ctx context.Context) error
- type ApprovalBinding
- type ApprovalDecision
- type ApprovalPlan
- type ApprovalRecord
- type ApprovalStore
- type AuthMode
- type Caller
- type Cluster
- type Decision
- type Denied
- type Frame
- type Gate
- type Hop
- type InvokeResult
- type KV
- type Mode
- type NodeKey
- type Option
- type Platform
- type PresenceStore
- type RateLimitConfig
- type RateLimitResult
- type RateLimiter
- type Registry
- func (r *Registry) Answer(corrID string, res InvokeResult)
- func (r *Registry) Invoke(ctx context.Context, key NodeKey, frame Frame, timeout time.Duration) (InvokeResult, error)
- func (r *Registry) List(org string) []*Session
- func (r *Registry) PeerHandler() http.Handler
- func (r *Registry) Register(s *Session) error
- func (r *Registry) Run(ctx context.Context)
- func (r *Registry) Unregister(connID string)
- type Session
- type SystemRunParams
- type WSOptions
Constants ¶
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" 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.
const ( CommandSystemRun = "system.run" CommandSystemRunPrepare = "system.run.prepare" CommandSystemWhich = "system.which" CommandSystemNotify = "system.notify" CommandBrowserProxy = "browser.proxy" )
Commands the gateway itself knows by name.
const ( RateLimitScopeDefault = "default" 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.
const ( ScopeOperatorAdmin = "operator.admin" ScopeOperatorApprovals = "operator.approvals" )
Scopes that let a caller speak for the approval system itself.
const ApprovalHostNode = "node"
ApprovalHostNode is the only host an approval may bind to for a node invoke.
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.
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 ¶
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
IsDangerous reports whether a command is high risk.
func IsHostExec ¶
IsHostExec reports whether a command runs a program on the node's machine.
func NodeWS ¶
NodeWS returns the handler nodes dial. Mount it on a GET route; reg is where the sessions it opens become addressable.
func PlatformCommands ¶
PlatformCommands returns a platform's defaults, sorted.
func RequiresTokenForInstall ¶
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.
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 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 ¶
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 ¶
Denied is a gate refusal. It carries a stable code so the caller can answer with it, and it survives a peer forward.
type Frame ¶
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 ¶
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 ¶
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 ¶
NodeKey addresses a node. Both fields are required; there is no way to name a node without naming its tenant.
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 ¶
WithCluster makes this registry reachable from its peers. Omitting it is the single-replica registry, unchanged.
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.
func ClassifyPlatform ¶
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 ¶
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.