Documentation
¶
Overview ¶
Package registry owns Harbor's Agent Registry — the in-process, per-runtime-instance subsystem that owns the *registration identity* of agents (RFC §6.16, D-059 / D-060).
There is no central Harbor service and there must not be one: every `harbor` process (and every embedding of the library) has its own AgentRegistry, persisted via that instance's configured StateStore driver (in-mem / SQLite / Postgres — the §9 persistence triad, behind the §4.4 seam). The registry consumes the *existing* StateStore seam (D-027); it does not define a driver seam of its own.
The three-ID model (D-059) ¶
Each registered agent carries three identifiers, each answering a different question:
- agent_id — "which logical agent." Minted once at first registration (ULID), persisted, rehydrated on restart. Runtime-instance-local, collision-free by construction; never assumed globally unique.
- incarnation — "which boot of it." Ephemeral; bumps on every process start (every Register of an already-known agent).
- version_hash — "which configuration." Deterministic content hash over (prompt set, tool set + schemas, planner config, model policy); bumps ONLY when configuration content changes.
A plain restart yields the same agent_id + same version_hash + a new incarnation; a restart after a configuration edit bumps both incarnation and version_hash. restart != recreate: re-registering the same RegistrationKey keeps the agent_id and the StateStore record; Deregister-then-Register of the same key mints a fresh agent_id because it is a new logical entity.
agent_id is NOT an isolation principal ¶
Harbor's isolation boundary is and stays the tuple (tenant, user, session) (+ run for the quadruple). An agent is a runtime *entity* that runs *within* (tenant, user, session); it does not widen the isolation boundary. The registry's storage methods scope by the tuple, NEVER by agent_id — agent_id is a registration identity, not a WHERE-clause isolation filter (D-059, AGENTS.md §6).
Two creation cases (D-060) ¶
- Locally-hosted agent — the runtime instance is running the agent; Register mints a local agent_id.
- Connect-to-remote agent — the agent runs in another Harbor (or is any A2A-speaking peer); RegisterRemote assigns a *handle* (an agent_id local to this instance, Hosting == HostingRemote) and stores an AgentCardRef pointing at the canonical A2A AgentCard, owned by the remote operator.
Fleet privilege tiers (D-066) ¶
Fleet *observation* (Get / List / Inspect / ReportHealth) requires the ordinary identity scope. Fleet *control* (Pause / Drain / Restart / ForceStop) requires a distinct, more-elevated control-scope claim — a leaked read-only token must not be able to force-stop a fleet. Every fleet-control command is audit-redacted and emitted. The control-scope claim is trust-based in V1 (mirrors the events package's Phase-05 Admin claim); cryptographic verification arrives with Protocol auth (Phase 61).
Concurrent reuse (D-025) ¶
*Registry is a compiled reusable artifact: one shared instance is safe under N concurrent registrations / lookups / control commands. Mutable state is guarded; per-call state lives in ctx, never on the registry.
Index ¶
- Constants
- Variables
- func HasControlScope(ctx context.Context) bool
- func IsValidHealth(h Health) bool
- func VersionHash(cfg AgentConfig) (string, error)
- func WithControlScope(ctx context.Context) context.Context
- type AgentConfig
- type AgentControlPayload
- type AgentDeregisteredPayload
- type AgentHealthPayload
- type AgentRecord
- type AgentRegisteredPayload
- type AgentRegistry
- type AgentRestartedPayload
- type AgentSnapshot
- type Clock
- type Deps
- type Health
- type Hosting
- type Option
- type RegisterOptions
- type Registry
- func (r *Registry) Close(_ context.Context) error
- func (r *Registry) Deregister(ctx context.Context, agentID string) error
- func (r *Registry) Drain(ctx context.Context, agentID string, reason string) error
- func (r *Registry) ForceStop(ctx context.Context, agentID string, reason string) error
- func (r *Registry) Get(ctx context.Context, agentID string) (*AgentRecord, error)
- func (r *Registry) Inspect(ctx context.Context, agentID string) (*AgentSnapshot, error)
- func (r *Registry) List(ctx context.Context) ([]AgentRecord, error)
- func (r *Registry) Pause(ctx context.Context, agentID string, reason string) error
- func (r *Registry) Register(ctx context.Context, key string, cfg AgentConfig, opts RegisterOptions) (*AgentRecord, error)
- func (r *Registry) RegisterRemote(ctx context.Context, key string, cardRef string, opts RegisterOptions) (*AgentRecord, error)
- func (r *Registry) ReportHealth(ctx context.Context, agentID string, h Health) error
- func (r *Registry) Restart(ctx context.Context, agentID string, reason string) error
- type ToolDescriptor
Constants ¶
const ( // EventTypeAgentRegistered — a NEW logical agent was registered // (incarnation 1). Payload: AgentRegisteredPayload. EventTypeAgentRegistered events.EventType = "agent.registered" // EventTypeAgentRestarted — a KNOWN logical agent was re-registered // (incarnation bumped; restart != recreate). Payload: // AgentRestartedPayload. EventTypeAgentRestarted events.EventType = "agent.restarted" // EventTypeAgentHealth — an agent's Health was reported / changed. // Payload: AgentHealthPayload. EventTypeAgentHealth events.EventType = "agent.health" // EventTypeAgentDrained — a Drain fleet-control command was issued. // Payload: AgentControlPayload. EventTypeAgentDrained events.EventType = "agent.drained" // EventTypeAgentDeregistered — an agent's record was removed. // Payload: AgentDeregisteredPayload. EventTypeAgentDeregistered events.EventType = "agent.deregistered" // EventTypeAgentPaused — a Pause fleet-control command was issued. // Payload: AgentControlPayload. EventTypeAgentPaused events.EventType = "agent.paused" // EventTypeAgentRestartRequested — a Restart fleet-control command // was issued. Distinct from agent.restarted (which is the registry // observing a re-registration); this is the operator REQUESTING a // restart. Payload: AgentControlPayload. EventTypeAgentRestartRequested events.EventType = "agent.restart_requested" // EventTypeAgentForceStopped — a ForceStop fleet-control command // was issued. Payload: AgentControlPayload. EventTypeAgentForceStopped events.EventType = "agent.force_stopped" )
Agent lifecycle + fleet-control event types. Each is registered with the events package's exhaustive registry via init() so Publish accepts them without ErrUnknownEventType. Subscribers (the Console Agents page lens, later phases) filter on these via events.Filter.Types.
Every agent.* event carries the registration agent_id in its payload (RFC §6.16 "Events").
Variables ¶
var ( // ErrIdentityRequired — a method was called with a context whose // identity triple is missing or incomplete. Identity is mandatory; // the registry fails closed (AGENTS.md §6 rule 9). There is no // opt-out knob. ErrIdentityRequired = errors.New("registry: identity triple incomplete") // ErrControlScopeRequired — a fleet-control command (Pause / Drain // / Restart / ForceStop) was called without the elevated // control-scope claim. Fleet control is a distinct, more-elevated // privilege tier than fleet observation (D-066). ErrControlScopeRequired = errors.New("registry: fleet-control requires elevated control-scope claim") // ErrAgentNotFound — Get / Inspect / ReportHealth / Deregister / // any control command targeting an agent_id with no record under // the ctx identity. ErrAgentNotFound = errors.New("registry: agent not found") // ErrAgentExists — reserved for a future explicit-id registration // path; not returned by the V1 Register/RegisterRemote flow (which // rehydrates on a known key rather than erroring). ErrAgentExists = errors.New("registry: registration key already active") // ErrRegistryClosed — any operation called after Close. ErrRegistryClosed = errors.New("registry: registry is closed") // ErrInvalidConfig — Register / RegisterRemote called with a // structurally invalid argument (empty registration key, empty // cardRef for a remote agent, invalid Health). ErrInvalidConfig = errors.New("registry: invalid agent config") )
Sentinel errors. Callers compare via errors.Is.
Functions ¶
func HasControlScope ¶
HasControlScope reports whether ctx carries the elevated fleet-control-scope claim.
func IsValidHealth ¶
IsValidHealth reports whether h is one of the canonical Health values.
func VersionHash ¶
func VersionHash(cfg AgentConfig) (string, error)
VersionHash computes the deterministic content hash of an AgentConfig (RFC §6.16 "version_hash"; algorithm settled in D-068).
The hash answers "which configuration" — it bumps iff the configuration *content* changes and is otherwise stable across a plain restart. To make it deterministic regardless of caller-side ordering, the config is canonicalised before hashing:
- Prompts slice is sorted (prompt order is not semantic).
- Tools slice is sorted by (Name, SchemaDigest) (binding order is not semantic).
- PlannerConfig / ModelPolicy maps are encoded with sorted keys (Go's encoding/json already sorts map keys, but the canonical struct makes the contract explicit and robust to a future encoder swap).
The canonical form is JSON-encoded and SHA-256 hashed; the result is lowercase hex. The function is pure: same content in, same hash out, no package-level state, safe for concurrent use (D-025).
func WithControlScope ¶
WithControlScope attaches the elevated fleet-control-scope claim to ctx. Fleet-control commands (Pause / Drain / Restart / ForceStop) require this claim; fleet-observation methods do not.
The claim is trust-based in V1 — it is set by the Protocol auth layer once it has verified an operator's elevated scope claim (Phase 61). Until then, any caller that sets it is trusted; the audit emit on every control command makes abuse retroactively detectable, mirroring the events package's Admin-claim model.
Types ¶
type AgentConfig ¶
type AgentConfig struct {
// Prompts is the agent's prompt set. Order is not semantic — the
// hash canonicaliser sorts before hashing.
Prompts []string
// Tools is the agent's tool bindings (name + schema digest). Order
// is not semantic — sorted before hashing.
Tools []ToolDescriptor
// PlannerConfig is the planner's configuration key/value set.
PlannerConfig map[string]string
// ModelPolicy is the model-selection / model-policy key/value set.
ModelPolicy map[string]string
}
AgentConfig is the configuration content version_hash is derived from (RFC §6.16). It is caller-supplied at Register time and hashed (after canonicalisation) — the registry never stores it as a user-facing persona object; only the resulting VersionHash is persisted on the AgentRecord. version_hash bumps iff this content changes (D-068: SHA-256 over canonical JSON).
type AgentControlPayload ¶
type AgentControlPayload struct {
events.SafeSealed
AgentID string
Command string
Reason string // already passed through audit.Redactor
IssuedAt int64 // unix nanoseconds
}
AgentControlPayload reports a fleet-control command (Pause / Drain / Restart / ForceStop). Command is one of "pause" / "drain" / "restart" / "force_stop". Reason is the operator-supplied, audit-redacted reason string — callers MUST NOT pass tool args, raw user input, or secret-shaped material; the registry runs Reason through the audit.Redactor before this payload is built, and the bus does not re-redact SafePayload types (D-020 / D-028).
SafePayload by construction: every field is either a closed enum, an id, a timestamp, or an already-redacted string.
type AgentDeregisteredPayload ¶
type AgentDeregisteredPayload struct {
events.SafeSealed
AgentID string
RegistrationKey string
DeregisteredAt int64 // unix nanoseconds
}
AgentDeregisteredPayload reports an agent record removal. SafePayload by construction.
type AgentHealthPayload ¶
type AgentHealthPayload struct {
events.SafeSealed
AgentID string
Health string // string form of Health
ReportedAt int64 // unix nanoseconds
}
AgentHealthPayload reports a Health report / change. SafePayload by construction — Health is a closed enum.
type AgentRecord ¶
type AgentRecord struct {
// AgentID is the stable registration identity — a ULID minted once
// at first registration, persisted, rehydrated on restart. For a
// HostingRemote agent it is a *handle*: runtime-instance-local,
// never assumed globally unique.
AgentID string
// Incarnation bumps on every process start (every Register of an
// already-known RegistrationKey). incarnation == 1 is the first
// registration.
Incarnation uint64
// VersionHash is the deterministic content hash of the agent's
// configuration (D-068). Stable across a plain restart; bumps iff
// configuration content changed. Empty for HostingRemote agents
// (the configuration is owned by the remote operator).
VersionHash string
// RegistrationKey is the operator-stable logical-agent key. It is
// what distinguishes restart (same key → same agent_id) from
// recreate (Deregister + Register of the same key → fresh
// agent_id). It is NOT an isolation principal.
RegistrationKey string
// Identity is the (tenant, user, session) triple the agent is
// registered within. The registry scopes ALL storage by this
// triple — never by AgentID.
Identity identity.Identity
// Hosting discriminates locally-hosted vs connect-to-remote.
Hosting Hosting
// AgentCardRef references the canonical A2A AgentCard for a
// HostingRemote agent. Empty for HostingLocal agents.
AgentCardRef string
// DisplayName is the operator-facing cosmetic label.
DisplayName string
// Health is the last-reported / last-set operational health.
Health Health
// RegisteredAt is the wall-clock time of the FIRST registration
// (incarnation 1). Stable across restarts.
RegisteredAt time.Time
// UpdatedAt is the wall-clock time of the most recent mutation
// (re-registration, health report, control command).
UpdatedAt time.Time
}
AgentRecord is the persisted registration-identity record for one agent. It is the unit the registry stores (through the StateStore) and the shape the Console Agents page renders (through the Protocol, later phases).
AgentRecord is a value type; the registry returns copies so callers cannot mutate registry-owned state.
type AgentRegisteredPayload ¶
type AgentRegisteredPayload struct {
events.SafeSealed
AgentID string
RegistrationKey string
Incarnation uint64
VersionHash string
Hosting string // string form of Hosting
RegisteredAt int64 // unix nanoseconds
}
AgentRegisteredPayload reports a first registration (incarnation 1). Carries the registration agent_id; the identity triple lives on the Event itself, so it is intentionally NOT duplicated here.
SafePayload by construction — no secret-shaped fields. DisplayName / RegistrationKey are operator-controlled cosmetic labels, not secret-shaped material.
type AgentRegistry ¶
type AgentRegistry interface {
// Register mints (or rehydrates) a locally-hosted agent's
// registration identity.
//
// - First registration of `key`: mints a fresh agent_id (ULID),
// incarnation = 1, version_hash = hash(cfg), emits
// agent.registered.
// - Re-registration of a known `key` (restart): keeps the
// agent_id, bumps incarnation, recomputes version_hash (stable
// iff cfg content unchanged), emits agent.restarted.
//
// Returns a copy of the resulting AgentRecord.
Register(ctx context.Context, key string, cfg AgentConfig, opts RegisterOptions) (*AgentRecord, error)
// RegisterRemote registers a connect-to-remote agent (D-060). The
// local agent_id is a *handle*; cardRef references the canonical
// A2A AgentCard owned by the remote operator. Semantics mirror
// Register (first vs re-registration of `key`), but no version_hash
// is computed — the configuration is owned remotely.
RegisterRemote(ctx context.Context, key string, cardRef string, opts RegisterOptions) (*AgentRecord, error)
// Get returns the AgentRecord for agentID, scoped to the ctx
// identity. Returns a wrapped ErrAgentNotFound when no record
// exists for that (identity, agentID). Fleet observation — ordinary
// identity scope.
Get(ctx context.Context, agentID string) (*AgentRecord, error)
// List returns every AgentRecord registered under the ctx
// identity, in agent_id order. One identity's view never includes
// another identity's agents. Fleet observation — ordinary identity
// scope.
List(ctx context.Context) ([]AgentRecord, error)
// Inspect returns the read-side AgentSnapshot for agentID. Fleet
// observation — ordinary identity scope.
Inspect(ctx context.Context, agentID string) (*AgentSnapshot, error)
// ReportHealth updates the agent's Health and emits agent.health.
// Fleet observation tier — health is reported BY the agent, not a
// privileged control command.
ReportHealth(ctx context.Context, agentID string, h Health) error
// Deregister removes the agent's record and emits
// agent.deregistered. A subsequent Register of the same
// RegistrationKey mints a FRESH agent_id (recreate != restart).
Deregister(ctx context.Context, agentID string) error
// Pause requests the agent pause. FLEET CONTROL — requires the
// elevated control-scope claim (ErrControlScopeRequired otherwise);
// audit-redacted and emitted.
Pause(ctx context.Context, agentID string, reason string) error
// Drain requests the agent drain in-flight work and accept no new
// work; sets Health = HealthDraining. FLEET CONTROL — requires the
// elevated control-scope claim; audit-redacted and emitted as
// agent.drained.
Drain(ctx context.Context, agentID string, reason string) error
// Restart requests the agent restart. FLEET CONTROL — requires the
// elevated control-scope claim; audit-redacted and emitted.
Restart(ctx context.Context, agentID string, reason string) error
// ForceStop force-stops the agent; sets Health = HealthStopped.
// FLEET CONTROL — requires the elevated control-scope claim;
// audit-redacted and emitted.
ForceStop(ctx context.Context, agentID string, reason string) error
// Close releases the registry. Idempotent. Subsequent operations
// return ErrRegistryClosed. The V1 registry owns no long-lived
// goroutine; Close exists for interface symmetry and future-
// proofing.
Close(ctx context.Context) error
}
AgentRegistry is the public surface every consumer talks to — the Protocol surface (Phase 54+), the Console Agents page lens (Phase 72–75), and Phase 30's agent-bound OAuth (which keys tokens by the registration AgentID). One concrete impl ships in Phase 53a (*Registry); there is no driver pluralism at the registry layer — driver pluralism lives at the StateStore layer (D-027).
Identity is mandatory on every method: a context whose identity triple is missing or incomplete is rejected with a wrapped ErrIdentityRequired before any storage is touched (fail-closed).
type AgentRestartedPayload ¶
type AgentRestartedPayload struct {
events.SafeSealed
AgentID string
RegistrationKey string
Incarnation uint64
VersionHash string
VersionHashChanged bool
RestartedAt int64 // unix nanoseconds
}
AgentRestartedPayload reports a re-registration of a known agent (incarnation bumped). VersionHashChanged distinguishes "restarted, no change" from "restarted after a config edit" — the Console renders these differently. SafePayload by construction.
type AgentSnapshot ¶
type AgentSnapshot struct {
AgentRecord
// Local is true iff Hosting == HostingLocal. Convenience for the
// Console lens so it does not string-compare Hosting.
Local bool
}
AgentSnapshot is the read-side projection returned by Inspect. In V1 it carries the AgentRecord plus a Local convenience boolean derived from Hosting; later phases may extend it with derived operational fields (active task count, last-seen, etc.) sourced from other subsystems.
type Clock ¶
Clock abstracts time so tests are deterministic without time.Sleep (AGENTS.md §11). Production code uses realClock.
type Deps ¶
type Deps struct {
// Store is the per-runtime-instance StateStore. Driver pluralism
// (in-mem / SQLite / Postgres) lives here, not at the registry
// layer (D-027 / D-060).
Store state.StateStore
// Bus is the typed event bus the registry emits agent.* events on.
Bus events.EventBus
// Redactor redacts the operator-supplied Reason string on every
// fleet-control command before it is emitted (D-020 / D-066).
Redactor audit.Redactor
}
Deps bundles the collaborators the Registry needs. All three are mandatory — the registry fails loudly at construction if any is nil (AGENTS.md §5 "fail loudly"; identity/audit are mandatory).
type Health ¶
type Health string
Health is the operational-health enum the registry tracks per agent. It is push-reported via ReportHealth and mutated by fleet-control commands (Drain → HealthDraining, ForceStop → HealthStopped); the registry runs no health-polling goroutine in V1.
const ( // HealthUnknown — the initial state at registration; no health has // been reported yet. HealthUnknown Health = "unknown" // HealthHealthy — the agent reported itself operational. HealthHealthy Health = "healthy" // HealthDegraded — the agent reported partial / impaired operation. HealthDegraded Health = "degraded" // HealthDraining — a Drain control command is in effect; the agent // is finishing in-flight work and accepting no new work. HealthDraining Health = "draining" // HealthStopped — a ForceStop control command was issued, or the // agent reported itself stopped. HealthStopped Health = "stopped" )
type Hosting ¶
type Hosting string
Hosting discriminates the two creation cases (D-060).
const ( // HostingLocal — the agent is hosted by this runtime instance; the // agent_id was minted here and this instance is authoritative. HostingLocal Hosting = "local" // HostingRemote — the agent runs elsewhere (another Harbor instance // or any A2A-speaking peer). The local agent_id is a *handle*; the // canonical identity is the remote A2A AgentCard referenced by // AgentRecord.AgentCardRef. HostingRemote Hosting = "remote" )
type RegisterOptions ¶
type RegisterOptions struct {
// DisplayName is an operator-facing label. Not load-bearing for
// identity; purely cosmetic for the Console Agents page.
DisplayName string
}
RegisterOptions carries the non-identity, non-config metadata for a registration. All fields are optional; the zero value is valid.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry is the StateStore-backed implementation of AgentRegistry. It is the single concrete impl in V1 — driver pluralism lives at the StateStore layer (D-027).
Concurrency model (D-025):
- The StateStore is itself concurrent-safe per D-027.
- mu serialises the read-modify-write of a per-identity index document (Register / RegisterRemote / Deregister) so two concurrent registrations under the same identity cannot lose an index entry. It is a single registry-wide mutex; per-identity striping is a future optimisation if contention is ever measured (AGENTS.md §5 — "RWMutex only when contention is measured").
- closed is an atomic flag; the registry owns no long-lived goroutine, so Close just flips it.
- No mutable per-run state lives on the struct: identity comes from ctx on every call, the index/record live in the StateStore.
func New ¶
New constructs a Registry. Store, Bus, and Redactor are all required.
There is no eager rehydration step: the StateStore is the source of truth and is read on every operation. "Rehydration on restart" is therefore automatic — a fresh *Registry over a durable StateStore (SQLite / Postgres) sees the prior process's agents; a fresh *Registry over a fresh in-mem store does not (the in-mem driver is dev-only and non-persistent — D-060).
func (*Registry) Close ¶
Close releases the registry. Idempotent. The V1 registry owns no long-lived goroutine, so Close just flips the closed flag.
func (*Registry) Deregister ¶
Deregister removes the agent's record and emits agent.deregistered. A subsequent Register of the same RegistrationKey mints a FRESH agent_id (recreate != restart).
func (*Registry) Drain ¶
Drain requests the agent drain; sets Health = HealthDraining. FLEET CONTROL.
func (*Registry) ForceStop ¶
ForceStop force-stops the agent; sets Health = HealthStopped. FLEET CONTROL.
func (*Registry) List ¶
func (r *Registry) List(ctx context.Context) ([]AgentRecord, error)
List returns every AgentRecord registered under the ctx identity, in agent_id order. One identity's view never includes another's.
func (*Registry) Register ¶
func (r *Registry) Register(ctx context.Context, key string, cfg AgentConfig, opts RegisterOptions) (*AgentRecord, error)
Register mints (or rehydrates) a locally-hosted agent's registration identity. See AgentRegistry.Register.
func (*Registry) RegisterRemote ¶
func (r *Registry) RegisterRemote(ctx context.Context, key string, cardRef string, opts RegisterOptions) (*AgentRecord, error)
RegisterRemote registers a connect-to-remote agent. The local agent_id is a handle; cardRef references the canonical A2A AgentCard. See AgentRegistry.RegisterRemote.
func (*Registry) ReportHealth ¶
ReportHealth updates the agent's Health and emits agent.health. Fleet-observation tier — health is reported BY the agent.
type ToolDescriptor ¶
ToolDescriptor identifies one tool binding for version_hash purposes. Name is the tool's catalog name; SchemaDigest is a caller-supplied digest of the tool's argument/result schema. The registry hashes these into version_hash — it does not interpret or validate them.