Documentation
¶
Overview ¶
Package auth provides a lightweight, role-based access control layer for kojo's HTTP API. Its purpose is "spoiler prevention" — keeping an agent from incidentally reading other agents' Persona / configuration when it curls the API on its own. It is NOT a security boundary against a malicious agent: an agent runs as the same OS user as kojo itself and can read other agents' files directly. See README for the threat model.
Index ¶
- Constants
- Variables
- func AgentFencingMiddleware(st AgentFencingStore, selfPeerID string, logger *slog.Logger) func(http.Handler) http.Handler
- func AllowNonOwner(p Principal, method, path string) bool
- func AuthMiddleware(resolver *Resolver) func(http.Handler) http.Handler
- func EnforceMiddleware(next http.Handler) http.Handler
- func OwnerOnlyMiddleware(h http.Handler) http.Handler
- func SplitAgentIDPath(path string) (id, sub string, ok bool)
- func TailnetIdentityMiddleware(cfg TailnetIdentityConfig) func(http.Handler) http.Handler
- func WithPrincipal(ctx context.Context, p Principal) context.Context
- type AgentFencingStore
- type Principal
- func (p Principal) CanDeleteOrReset(targetID string) bool
- func (p Principal) CanForkOrCreate() bool
- func (p Principal) CanMutateSelf(targetID string) bool
- func (p Principal) CanReadFull(targetID string) bool
- func (p Principal) CanRestartServer() bool
- func (p Principal) CanSetPrivileged() bool
- func (p Principal) IsAgent() bool
- func (p Principal) IsOwner() bool
- func (p Principal) IsPeer() bool
- type Resolver
- type Role
- type TailnetIdentityConfig
- type TokenStore
- func (s *TokenStore) AdoptAgentTokenFromPeer(agentID, rawToken string) error
- func (s *TokenStore) AgentToken(agentID string) (string, error)
- func (s *TokenStore) AutoRepairAgentToken(agentID string) (bool, error)
- func (s *TokenStore) EnsureAgentToken(agentID string) error
- func (s *TokenStore) LookupAgent(token string) (string, bool)
- func (s *TokenStore) OwnerHash() string
- func (s *TokenStore) OwnerToken() string
- func (s *TokenStore) ReissueAgentToken(agentID string) (string, error)
- func (s *TokenStore) RemoveAgentToken(agentID string)
- func (s *TokenStore) VerifyOwner(presented string) bool
Constants ¶
const HeaderNoIdempotencyCache = "X-Kojo-No-Idempotency-Cache"
HeaderNoIdempotencyCache is the response header any middleware sets to tell the idempotency wrapper NOT to save the captured response. Used by AgentFencing for its 409 wrong_holder response: the underlying lock state is transient, so caching the refusal would shadow a future successful retry.
Variables ¶
var ErrNodeKeyResolverNotReady = errors.New("auth: tsnet node key resolver not ready")
ErrNodeKeyResolverNotReady is returned by a TailnetIdentityConfig.Resolver to signal "the tsnet/tailscaled handle is not wired yet" — distinct from a real WhoIs failure or a genuinely unknown caller. The middleware uses it to refuse Hub-mode Owner-fallback during the brief window between server start and resolver wiring; a real `lc.WhoIs` error (tailscaled blip) or empty `w.Node` (tsnet view stale) still falls back to Owner so the operator's Dashboard keeps working through transient outages.
Callers that build the resolver closure should return ("", ErrNodeKeyResolverNotReady) when the underlying handle is nil instead of returning ("", nil) — the latter is reserved for "WhoIs returned no node" and is honored by Hub fallback.
ErrTokenRawUnavailable is returned by AgentToken when the agent has a stored token hash but the raw value is no longer available in memory (post-migration boot). Callers either accept this (the agent already has the token cached elsewhere) or re-issue via ReissueAgentToken.
Functions ¶
func AgentFencingMiddleware ¶ added in v0.101.0
func AgentFencingMiddleware(st AgentFencingStore, selfPeerID string, logger *slog.Logger) func(http.Handler) http.Handler
AgentFencingMiddleware refuses mutating requests from RoleAgent / RolePrivAgent / RolePeer principals when agent_locks.holder_peer does NOT match the local peer. Closes the §3.7 device-switch invariant the previous slice left open: once the orchestrator transfers the lock to a target peer, the source peer's agent runtime — and any stale Hub→source proxy write — MUST stop writing to the agent's tables.
Scope:
- RoleAgent / RolePrivAgent gated as before.
- RolePeer also gated: a Hub→peer agent proxy that lands on a host that no longer holds the lock must 409 rather than mutate state out from under the real holder. The fence is the load-bearing safeguard here because EnforceMiddleware admits /api/v1/agents/* for trusted RolePeer.
- RoleOwner / RoleGuest pass through (owner is admin; guest doesn't write to agent state).
- Read methods (GET / HEAD / OPTIONS) pass through. Lock holders rotate via complete; readers should still observe transient state without 409s.
- The agent's own /handoff/switch route is exempted because it IS the call that moves the lock away — refusing it would create a chicken-and-egg deadlock.
- Routes outside /api/v1/agents/{agentID}/ pass through. The gate is per-agent; cross-agent routes (groupdms, etc.) have their own membership checks.
Failure modes:
- lock row missing (ErrNotFound): the agent has no claimed lock yet (first boot, or recently released). v1 single-Hub trusts that AgentLockGuard will Acquire shortly; we pass through rather than block a fresh agent. A future multi- peer cluster might tighten this to "no lock = refuse".
- store read error: 503 with a generic message; the agent runtime should retry rather than barge through an unknown state.
- holder mismatch: 409 wrong_holder so the agent runtime knows to stop driving writes and (typically) shut itself down — the lock has moved.
func AllowNonOwner ¶
AllowNonOwner gates non-Owner principals against a small whitelist of API routes. Unrecognised paths are denied with 403, and recognised paths may further be denied based on Principal/method/target.
Wired via EnforceMiddleware on BOTH listeners:
- public (tsnet) listener — TailnetIdentityMiddleware stamps RoleOwner for every tailnet caller (Hub mode), including callers whose WhoIs is transiently unresolved, or RolePeer / Guest (peer mode). Owner short-circuits at the top, so this gate effectively runs only for the §3.7 RolePeer inter-peer surface and Guest fallthroughs.
- agent-facing (auth-required) loopback listener — AuthMiddleware resolves Bearer/X-Kojo-Token to Owner/Agent/PrivAgent, and this gate enforces per-route policy for non-Owner roles.
The intent is "default deny". Routes are grouped into three buckets:
- Public reads — info, directory, agent list/get, avatar.
- Self-scoped reads/writes — files, messages, tasks, credentials, memory, slackbot config, notify sources, group memberships, avatar upload, persona / metadata patch, MCP, PreCompact hook. Permitted only when the path's {id} matches the principal.
- Privileged-cross-agent — delete / reset / checkin / unarchive / reset-session. Permitted for self by Agent, or for any target by PrivAgent.
Owner-only routes (sessions, git, files browser, embedding, push, custom-models, group DM mutate-as-owner, fork, /privilege, generate-*) fall through and 403. Note: POST /api/v1/groupdms (group creation) is exposed to Agent / PrivAgent below — the handler then enforces the caller-in-memberIds invariant.
func AuthMiddleware ¶
AuthMiddleware resolves Authorization: Bearer / X-Kojo-Token to a Principal and passes it through ctx. It does NOT enforce per-route policy — that is the handler's responsibility (or a separate gate).
Skips the Bearer resolution when an earlier middleware (e.g. TailnetIdentityMiddleware) already attached a non-Guest principal so a paired peer's Tailnet-identified RolePeer doesn't get downgraded to Guest by the absence of a Bearer.
func EnforceMiddleware ¶
EnforceMiddleware gates /api/v1/* requests by Principal/method/path using AllowNonOwner. It runs after AuthMiddleware (which set the Principal in ctx). Static files and non-API paths pass through.
func OwnerOnlyMiddleware ¶
OwnerOnlyMiddleware tags every request as the Owner UNLESS an earlier middleware (e.g. TailnetIdentityMiddleware) already attached a non-Guest principal. The exception keeps the "Tailscale reach == Owner" UX for the kojo user from clobbering a paired peer's Tailnet-identified RolePeer on the same listener.
Used on the public (Tailscale) listener that the kojo user accesses from their phone — the user's UX is preserved (no token required) for everything except inter-peer requests that arrive pre-stamped.
func SplitAgentIDPath ¶ added in v0.101.0
SplitAgentIDPath parses /api/v1/agents/{id}{sub} into (id, sub, true) where sub starts with "/" or is empty. Returns ok=false if the path does not match.
func TailnetIdentityMiddleware ¶ added in v0.101.0
func TailnetIdentityMiddleware(cfg TailnetIdentityConfig) func(http.Handler) http.Handler
TailnetIdentityMiddleware authenticates inbound HTTP requests against the Tailscale identity of the calling node.
Chain order: this middleware runs FIRST (before any per-role gate) so a downstream OwnerOnly or Enforce check sees the Principal already stamped.
Resolution:
- Unsafe → stamp RoleOwner (Hub) or RolePeer (--peer) and call next. tsnet is not consulted.
- Resolver(remoteAddr) → NodeKey. If the resolver errors OR returns an empty NodeKey (tsnet's view of the tailnet hasn't refreshed for this caller yet — typical for a just-joined node or one whose key was recently rotated), the request: - on Hub mode (PromoteUnknownTailnetToOwner=true) → Owner, UNLESS the error wraps ErrNodeKeyResolverNotReady. The tsnet listener is only reachable over Tailscale, so the listener boundary itself is the trust gate; a transient WhoIs blip must not 403 the operator's UI. The ErrNodeKeyResolverNotReady exception covers the startup window where SetNodeKeyResolver has not run yet — an unidentified caller there has no claim to Owner. - on Peer mode (false) → falls through as Guest so a downstream middleware (the AuthMiddleware on the auth-required listener) can resolve a Bearer instead.
- NodeKey == SelfNodeKey → RoleOwner.
- PromoteUnknownTailnetToOwner true (Hub public listener) → RoleOwner. peer_registry is NOT consulted for the role; it is touched async as a liveness side-effect when the NodeKey matches a registered peer.
- PromoteUnknownTailnetToOwner false (peer-mode public listener) → consult peer_registry. Hit ⇒ RolePeer + sync TouchPeer. Miss / DB error ⇒ Guest (unapproved tailnet callers stay Guest; the policy layer 403s privileged surface).
Types ¶
type AgentFencingStore ¶ added in v0.101.0
type AgentFencingStore interface {
GetAgentLock(ctx context.Context, agentID string) (*store.AgentLockRecord, error)
}
AgentFencingStore is the minimal contract AgentFencingMiddleware needs from the DB layer. Lets the test fixtures pass a fake without standing up a full *store.Store.
type Principal ¶
type Principal struct {
Role Role
AgentID string // populated for RoleAgent / RolePrivAgent
PeerID string // populated for RolePeer (device_id from peer_registry); also stamped on RoleOwner when the Hub-public TailnetIdentityMiddleware's WhoIs lookup matches a paired peer, so events handlers can identify which paired-peer connection they're on without re-querying the registry
}
Principal identifies the actor behind a request.
func FromContext ¶
FromContext retrieves the Principal stashed in ctx by middleware. Defaults to RoleGuest when no principal is set.
func (Principal) CanDeleteOrReset ¶
CanDeleteOrReset returns true for delete/reset/unarchive/checkin/ reset-session ops. Owner: any. PrivAgent: any. Agent: self only. Peer: admitted — Hub proxy validated the original caller.
func (Principal) CanForkOrCreate ¶
CanForkOrCreate returns true only for the Owner. Forking copies persona/memory and would leak the source agent's full state, so it is kept Owner-only even for privileged agents. Bare creation is also Owner-only.
func (Principal) CanMutateSelf ¶
CanMutateSelf returns true if the principal may issue self-scoped mutations (PATCH, reset, etc.) against targetID. Peers pass through because the Hub's Enforce layer already authorised the original request before the proxy signed and forwarded it.
func (Principal) CanReadFull ¶
CanReadFull returns true if the principal can read the full record (Persona, Token-bearing fields, etc.) for the given target agent ID. Owners can read any. Agents can only read their own. Peers are admitted because the Hub's proxy already validated the original caller's identity before forwarding.
func (Principal) CanRestartServer ¶ added in v0.111.0
CanRestartServer returns true for principals allowed to trigger a daemon self-restart (POST /api/v1/system/restart): the Owner and privileged agents. Regular agents and peers are refused — a restart quiesces every agent on this host, not just the caller.
func (Principal) CanSetPrivileged ¶
CanSetPrivileged returns true only for the Owner. A privileged-agent must never be able to grant or revoke privilege.
func (Principal) IsAgent ¶
IsAgent returns true if the principal is bound to a specific agent (regular or privileged).
func (Principal) IsPeer ¶ added in v0.101.0
IsPeer reports whether the principal was authenticated via Tailnet identity (WhoIs over tsnet → peer_registry hit). RolePeer is scoped to inter-peer endpoints (cross-subscribe status feed, blob handoff fetch, device-switch orchestration) AND to proxied agent requests (§3.7 remoteAgentProxy: Hub forwards browser/ agent requests to the holder peer over tsnet). Handler-level guard methods (CanReadFull, CanMutateSelf, CanDeleteOrReset) admit IsPeer because the Hub already ran Enforce before proxying — re-blocking at the handler would 403 every proxied request.
type Resolver ¶
type Resolver struct {
// contains filtered or unexported fields
}
Resolver maps a Bearer token to a Principal.
func NewResolver ¶
func NewResolver(tokens *TokenStore, isPrivileged func(string) bool) *Resolver
NewResolver builds a Resolver from a TokenStore and a privilege predicate (agent.Manager.IsPrivileged).
type Role ¶
type Role int
Role identifies the actor behind an HTTP request after middleware resolution.
const ( // RoleGuest is the default role for unauthenticated requests on the // auth-required listener. Guests can only read directory entries. RoleGuest Role = iota // RoleAgent is a regular agent-bound principal. It can fully read its // own data and is limited to DirectoryEntry on other agents. RoleAgent // RolePrivAgent extends RoleAgent with the ability to delete/reset // other agents (but not fork or read their full data). RolePrivAgent // RolePeer authenticates a request that arrived from a // registered remote peer over tsnet (docs §3.10 / §3.7). The // principal's PeerID names the device_id from peer_registry; // the scope is the inter-peer surface plus proxied // agent/session routes — every other gate returns false. Set // by TailnetIdentityMiddleware on ServeAuthTsnet (peer-mode // primary listener) and on the Hub's public listener via // WhoIs → peer_registry; the legacy Ed25519-signed Bearer // stamping path is retired. --unsafe collapses the WhoIs // check and stamps RolePeer unconditionally for LAN/docker/CI. RolePeer // RoleOwner is the kojo user. It has full access to everything. RoleOwner )
type TailnetIdentityConfig ¶ added in v0.101.0
type TailnetIdentityConfig struct {
Resolver func(ctx context.Context, remoteAddr string) (string, error)
Store *store.Store
// SelfNodeKeyFunc returns the local kojo's own Tailscale
// NodeKey at request time. Reads through a sync.RWMutex on the
// Server so cmd/kojo can rewire the value after tsnet binds.
// May be nil — equivalent to a function that always returns
// "" (no self-Owner promotion).
SelfNodeKeyFunc func() string
// PromoteUnknownTailnetToOwner, when true, stamps RoleOwner on
// every tailnet caller that reaches the listener — both
// WhoIs-resolved callers and callers whose WhoIs is transiently
// unresolved (resolver error, empty Node, tsnet view lag). The
// listener boundary itself (tsnet.ListenTLS) is the trust gate,
// so a transient WhoIs blip never demotes the operator. The one
// exception is ErrNodeKeyResolverNotReady — startup before
// SetNodeKeyResolver wires the closure — which stays Guest so
// "no resolver by design" cannot turn into Owner-by-default.
// peer_registry is NOT consulted on the Hub's public listener:
// that listener is
// exclusively for the operator's UI — paired peer devices
// opening the Hub UI in a browser also arrive here, and there
// is no UX-meaningful distinction between "operator on the Hub
// host" and "operator on a paired tailnet device."
//
// The peer-mode daemon leaves this FALSE so it can still
// classify a tailnet caller as RolePeer via peer_registry for
// the §3.7 device-switch surface (Server.ServeAuthTsnet wires
// this middleware ahead of AuthMiddleware on the peer's
// primary tsnet listener). A stray tailnet node that hasn't
// been Approved stays Guest and falls through to the Bearer
// gate.
PromoteUnknownTailnetToOwner bool
// Unsafe collapses the entire WhoIs path. Every caller is
// stamped RoleOwner (UnsafeAsHub=true) or RolePeer (false).
Unsafe bool
UnsafeAsHub bool // true on Hub binary (RoleOwner), false on --peer (RolePeer)
ResolveDelay time.Duration
Logger *slog.Logger
// SelfDeviceID, when non-empty, demotes a peer_registry hit
// whose device_id matches this value back to Guest. The
// self-row carries the local NodeKey so other peers can dial
// us; without this check, a request looped back through tsnet
// (e.g. operator hits the peer's tailnet IP from the same
// host) would resolve to the self-row and be stamped RolePeer,
// silently bypassing the Bearer gate downstream. Empty
// disables the check (Hub public listener doesn't need it
// since PromoteUnknownTailnetToOwner short-circuits ahead of
// the lookup).
SelfDeviceID string
}
TailnetIdentityConfig configures TailnetIdentityMiddleware.
Resolver maps an incoming request's RemoteAddr to the calling Tailscale node's stable NodeKey (`nodekey:...`). nil disables tsnet identity resolution — the middleware then admits every caller as Guest unless `Unsafe` is true.
Store is the peer_registry handle the middleware queries to translate a NodeKey into a Principal.PeerID.
SelfNodeKey, when non-empty, marks the local kojo's own Tailscale node. A request arriving from that key is the operator hitting the daemon from the same host they run kojo on — it gets promoted to RoleOwner to preserve the "Tailscale reach == Owner" UX on the public listener.
Unsafe collapses every check: the middleware stamps RolePeer (peer-mode) or RoleOwner (hub-mode) onto every request without consulting tsnet or the store. Intended for `--unsafe` LAN / docker / CI deployments where the operator opts into trusting the listener boundary.
type TokenStore ¶
type TokenStore struct {
// contains filtered or unexported fields
}
TokenStore persists owner / agent token HASHES and keeps an in-memory hash → ID index for O(1) reverse lookup.
Phase 2c-2 slice 17: hashes now live in the kv table at (namespace="auth", key="owner.token" | "agent_tokens/<id>", scope=global, type=string). The legacy on-disk layout is retained as a fallback for v1 installs that booted under pre-cutover binaries:
<base>/owner.token — owner secret hash (legacy) <base>/agent_tokens/<id> — per-agent secret hash (legacy)
On first boot after the cutover, NewTokenStore mirrors any surviving disk file into kv (IfMatchAny so a peer-replicated row wins on collision) and best-effort unlinks the disk file. Steady-state reads come from kv; the disk fallback path runs only when kv has no row AND the legacy file is present.
On-disk / on-the-wire value format (unchanged across the cutover so a rollback could still parse the value):
"sha256:<64-hex-chars>"
Legacy (pre-Phase-5) files held the raw token verbatim. On first boot the store recognizes the legacy form (64 hex chars, no prefix), rewrites it as the hashed form (now in kv, not on disk), and keeps the raw value in memory for THIS boot only so the existing console URL print (cmd/kojo) and agent-side env injection still work without a forced re-issue. From the next boot on, the raw token is unrecoverable from kv — only the operator's cached value (browser localStorage, agent env) keeps working. To force-re-issue a token, delete the kv row (or use RemoveAgentToken) and restart.
The store does NOT depend on the agent package; agent.Manager is the caller that drives EnsureAgentToken / RemoveAgentToken on agent lifecycle events.
func NewTokenStore ¶
NewTokenStore initializes a store rooted at base. The kv handle (Phase 2c-2 slice 17) is the canonical backing; the disk layout at base is retained as a legacy fallback for v1 installs that booted under pre-cutover binaries. Pass nil for kv in tests that only exercise the disk path.
The owner token is created on first use if absent unless overrideOwner is non-empty (in which case overrideOwner is treated as the canonical owner token and is *not* persisted — neither to kv nor to disk).
func (*TokenStore) AdoptAgentTokenFromPeer ¶ added in v0.101.0
func (s *TokenStore) AdoptAgentTokenFromPeer(agentID, rawToken string) error
AdoptAgentTokenFromPeer installs a raw token sent by another peer during a §3.7 device-switch agent-sync. The hash is computed locally, persisted to kv, and added to the in-memory verifier so the post-handoff PTY (whose $KOJO_AGENT_TOKEN was stamped by the source peer) authenticates here.
Semantics:
- Idempotent: a sync that delivers the same raw token we already hold is a no-op (no kv write, in-memory state unchanged).
- Source-wins: a sync that delivers a DIFFERENT raw token overwrites the local hash. Any prior raw token this peer had cached for the agent is dropped — the orchestrator intends the new token to be the only one in flight.
- The raw is added to rawByID so a subsequent PTY spawn can read it back via AgentToken (no re-issue needed).
Threadsafe: takes the write lock for the entire call.
func (*TokenStore) AgentToken ¶
func (s *TokenStore) AgentToken(agentID string) (string, error)
AgentToken returns the raw token for the given agent ID if one is available in memory (this boot generated or migrated it). Otherwise the store does not know the raw value and returns ("", error) so a caller can decide whether to re-issue. To force a re-issue, call ReissueAgentToken — it atomically swaps the kv hash and in-memory verifier under one lock without exposing a verifier gap, unlike the older RemoveAgentToken+AgentToken pattern.
func (*TokenStore) AutoRepairAgentToken ¶ added in v0.111.0
func (s *TokenStore) AutoRepairAgentToken(agentID string) (bool, error)
AutoRepairAgentToken is the §3.7 device-switch finalize helper for targets that received NO raw token in the sync payload (the source only held the kv hash after a restart). It ensures this peer ends up with a usable raw $KOJO_AGENT_TOKEN:
- raw already available (or freshly generated for an agent with no prior hash): nothing to repair, returns (false, nil).
- hash-only (ErrTokenRawUnavailable): re-issues via ReissueAgentToken — kv hash CAS-swapped, in-memory verifier + raw updated — and returns (true, nil).
- anything else: (false, err).
This replaces the old "manual re-issue required" operator step.
func (*TokenStore) EnsureAgentToken ¶
func (s *TokenStore) EnsureAgentToken(agentID string) error
EnsureAgentToken is a convenience wrapper used at agent-create time. Unlike AgentToken, it tolerates ErrTokenRawUnavailable: when a token already exists on disk we don't need to know its raw value to guarantee "the agent has a token".
func (*TokenStore) LookupAgent ¶
func (s *TokenStore) LookupAgent(token string) (string, bool)
LookupAgent returns the agent ID associated with the given raw token, if any. Hash is computed and matched in constant time against every stored agent hash — this is O(N) but agent count is small.
func (*TokenStore) OwnerHash ¶ added in v0.101.0
func (s *TokenStore) OwnerHash() string
OwnerHash returns the SHA-256 hash of the owner token (hex). Always populated.
func (*TokenStore) OwnerToken ¶
func (s *TokenStore) OwnerToken() string
OwnerToken returns the raw owner token if available on this boot, otherwise the empty string. Available scenarios:
- Fresh install (file just created)
- Legacy migration boot (file rewritten from raw to hash)
- KOJO_OWNER_TOKEN override
On any subsequent boot from a hash-only file, OwnerToken returns "". Callers that just want to verify a presented token MUST use VerifyOwner instead.
func (*TokenStore) ReissueAgentToken ¶ added in v0.101.0
func (s *TokenStore) ReissueAgentToken(agentID string) (string, error)
ReissueAgentToken atomically rotates the agent's token: CAS-replaces the existing kv row with a fresh hash, then swaps in-memory entries. Holds the write lock for the entire operation so concurrent callers for the same agent serialize and the second caller observes the freshly-issued raw value (rather than racing a second rotation that would invalidate the first caller's raw before it could be injected into a PTY env).
The kv row's hash is compared against this peer's local idIndex before CAS — if they disagree, another peer rotated and we adopt their hash instead of overwriting it. ETag-only CAS isn't enough: a successful peer rotation produces a fresh ETag, our GetKV reads that fresh ETag, and an unguarded saveAgentTokenKV would CAS- succeed and silently overwrite the peer's newly-issued hash — breaking the peer's PTY whose $KOJO_AGENT_TOKEN was just minted.
Returns the raw token kojo can inject into $KOJO_AGENT_TOKEN, or an error if persisting the new hash fails. In peer mode this causes the verifier hash in kv to change — other peers' in-memory verifiers do NOT hot-reload, so tokens issued here are valid on THIS peer until the others restart and re-read kv. Acceptable for the post-migration single-host case (the only place the current callback drives this); cluster-wide raw-token sync is a separate design problem.
func (*TokenStore) RemoveAgentToken ¶
func (s *TokenStore) RemoveAgentToken(agentID string)
RemoveAgentToken deletes the kv row, in-memory entries, and any surviving legacy disk file for an agent. Safe to call on an unknown ID. Invalid IDs are ignored.
func (*TokenStore) VerifyOwner ¶ added in v0.101.0
func (s *TokenStore) VerifyOwner(presented string) bool
VerifyOwner returns true if presented matches the stored owner token hash. Constant-time comparison prevents timing-leak side channels.