a2a

package
v0.22.162 Latest Latest
Warning

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

Go to latest
Published: May 25, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Overview

Package a2a — Agent2Agent protocol surface for clawtool (ADR-024). Phase 1 ships only the Agent Card serializer + the JSON shape the protocol wants advertised at /.well-known/agent-card.json. mDNS announce, HTTP server, peer discovery, and capability tier enforcement layer on top in phase 2+.

We adopt Google A2A's wire format (now Linux Foundation; github.com/a2aproject/A2A) verbatim rather than inventing one, per ADR-007 (wrap, don't reinvent). The Card describes *what* this clawtool instance can do (capabilities + skills + auth); it deliberately does NOT enumerate every aggregated tool — per A2A's opacity model, peers see the agent's contract, not its internal toolset.

Package a2a — circle key (ADR-024 cross-device trust).

A "circle" is the set of a user's own machines that should see each other's agents. The circle key is a pre-shared secret the user sets once on every device they own (think Tailscale auth key / Syncthing shared folder). Possessing it authorizes the READ-ONLY cross-device peer endpoints — listing a remote peer's agents — and nothing else: it never unlocks /v1/send_message or /mcp, which execute code and stay strictly bearer-token gated.

Trust model rationale: mDNS discovery is LAN-public (any device can see that a clawtool exists and read its capability card), but the agent inventory is not broadcast — a peer must prove circle membership to enumerate it. No key configured ⇒ cross-device agent enumeration is simply disabled, so nothing leaks by default.

Package a2a — peer inbox. The discovery half (registry.go) surfaces *who* is on the host; the inbox half (this file) is *how they talk*. Each peer has an in-memory mailbox; senders enqueue via POST /v1/peers/{id}/messages, recipients drain via GET /v1/peers/{id}/messages or `clawtool peer inbox`.

Wire shape mirrors repowire/repowire/protocol/messages.py (Query / Response / Notification / Broadcast) so a runtime hook polling once per UserPromptSubmit can surface pending messages as additionalContext without inventing its own format.

Persistence: each peer's inbox is mirrored to ~/.config/clawtool/peers.d/<peer_id>.inbox.json on every mutation. A daemon crash mid-flight loses at most the last in-flight message; the rest survive a restart. Soft cap at 256 messages per peer — overflow drops the OLDEST so a chatty sender can't OOM the daemon. New peers start empty.

Package a2a — mDNS announce + browse (Phase 2a of ADR-024).

Two clawtool daemons on the same LAN broadcast `_clawtool._tcp.local` via Zeroconf/Bonjour and parallel browse the same service type to populate the existing same-host peer Registry. TXT records carry the agent_card URL + peer_id + product version so a discovering peer can dial the announcer's Agent Card without a second DNS round trip.

Library choice: github.com/grandcat/zeroconf — idiomatic Go wrapper, MIT, widely deployed. ADR-024 names it as the de-facto pick for clawtool's mDNS surface.

Lifecycle: StartAnnounce + StartBrowse return idempotent handles (second call with the same peer_id returns the existing instance). Both honour context cancellation; Close() on either is also idempotent. The daemon boot path in internal/server/http.go owns the context — SIGTERM cancels it, which triggers graceful deregister of the announce + shutdown of the browse loop.

Self-discovery: the browse loop filters its own announcement by matching the `peer_id` TXT field against the local Announcer's peer_id. Without this, every daemon would add itself to its own registry. The match is best-effort (TXT parsing failures fall through to "not us") but covers the standard case where the Announcer published a well-formed record.

WSL2 quirks: the WSL2 NAT layer drops UDP multicast on 224.0.0.251:5353 by default. We don't try to work around that here — `clawtool doctor` prints a hint pointing operators at docs/networking.md for the firewall / Tailscale / static-peer recipes.

Package a2a — peer registry. Phase 1 of ADR-024's local-mesh half: every running clawtool / claude-code / codex / gemini / opencode session on this host registers into a single in-memory table keyed on a stable peer_id, so `clawtool a2a peers` can surface the live roster.

Mirrors the shape of repowire/daemon/peer_registry.py (prassanna-ravishankar/repowire) — the reference implementation for the discovery half. Differences from repowire:

  • Identity tuple: (backend, path, session_id, tmux_pane). The runtime-supplied session_id (claude-code's hook payload `.session_id`, etc.) is the primary disambiguator so two parallel sessions in the same cwd register as separate peers. tmux_pane is the secondary key when no session id exists.
  • REST + 30s heartbeat instead of WebSocket transport. The real-time push notifications repowire offers via websocket are deferred to Phase 2; Phase 1 ships the registry + polling because it's a fraction of the LoC and covers 80% of the operator value (visibility, cross-pane discovery).

Persistence: ~/.config/clawtool/peers.json (LF-delimited JSON, 0600). Atomic temp+rename writes so a crash mid-write doesn't leave a corrupt state file. Lazy repair on every read sweeps peers whose declared `path` no longer exists.

Index

Constants

View Source
const CircleKeyHeader = "X-Clawtool-Circle"

CircleKeyHeader is the HTTP header a peer presents to prove circle membership when fetching another peer's read-only endpoints.

View Source
const CurrentProtocolVersion = "0.2.0"

CurrentProtocolVersion is the A2A spec snapshot we conform to. Bump in lockstep with upstream as their stable snapshots advance.

View Source
const HeartbeatStaleAfter = 60 * time.Second

HeartbeatStaleAfter — peers whose last_seen is older than this are flipped to PeerOffline on the next list. Matches the 30 s heartbeat cadence we recommend in the registration docs (one missed heartbeat = grace period; two missed = offline).

View Source
const ServiceDomain = "local."

ServiceDomain is the DNS-SD domain. "local" is the only legal value for mDNS / Bonjour — we name it as a constant so the announce / browse pair don't drift.

View Source
const ServiceType = "_clawtool._tcp"

ServiceType is the DNS-SD service identifier every clawtool daemon advertises. Per ADR-024 §LAN discovery. Other clawtool instances browse the same string to find peers.

Variables

This section is empty.

Functions

func CircleKeyMatches added in v0.22.161

func CircleKeyMatches(expected, presented string) bool

CircleKeyMatches reports whether presented equals expected in constant time. Returns false when expected is empty (no circle configured ⇒ deny) so a missing key can never be matched by an empty header.

func CircleKeyPath added in v0.22.161

func CircleKeyPath() string

CircleKeyPath is where the shared secret lives (0600). Same XDG conventions as the daemon's token + state files.

func ClearCircleKey added in v0.22.161

func ClearCircleKey() error

ClearCircleKey removes the key file. Missing file is not an error.

func DefaultStatePath added in v0.22.36

func DefaultStatePath() string

DefaultStatePath returns ~/.config/clawtool/peers.json (or its XDG_CONFIG_HOME equivalent). Mirrors daemon.StatePath's convention so an operator inspecting the config dir sees daemon.json + peers.json side-by-side.

func GenerateCircleKey added in v0.22.161

func GenerateCircleKey() (string, error)

GenerateCircleKey returns a fresh 32-byte hex secret. It does NOT persist it — the caller decides whether to save locally and/or print it for the operator to copy onto their other devices.

func LoadCircleKey added in v0.22.161

func LoadCircleKey() (string, error)

LoadCircleKey returns the configured circle key (whitespace-trimmed), or "" with nil error when none is set — callers treat empty as "cross-device enumeration disabled".

func PeersStateDir added in v0.22.36

func PeersStateDir() string

PeersStateDir returns the canonical ~/.config/clawtool/peers.d directory used by both the daemon (per-peer inbox files written by this package) and the CLI's `clawtool peer` verb (per-session id pointer files). One layout, one helper — exported so callers outside this package don't reinvent the path-resolution dance.

On-disk layout:

peers.d/<session>.id            — CLI's session→peer_id pointer
peers.d/<peer_uuid>.inbox.json  — daemon's per-peer mailbox

func SaveCircleKey added in v0.22.161

func SaveCircleKey(key string) error

SaveCircleKey persists key atomically at 0600. A blank key is rejected — use ClearCircleKey to remove membership.

func SetGlobal added in v0.22.36

func SetGlobal(r *Registry)

SetGlobal installs the process-wide registry. Caller — typically internal/server/server.go's buildMCPServer — does this once at boot. Passing nil clears it (used by daemon shutdown).

func WaitForBrowseSettle added in v0.22.157

func WaitForBrowseSettle(d time.Duration)

WaitForBrowseSettle is a test-helper barrier — blocks until the browser has had a chance to drain its initial discovery burst. Production callers don't need it (the registry is updated lazily on every read). Lives here rather than the _test.go file because zeroconf's discovery latency varies by platform; centralising the wait avoids per-test tuning.

Types

type Announcer added in v0.22.157

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

Announcer is the running mDNS announce handle. Returned by StartAnnounce; the caller holds it for the daemon's lifetime and invokes Close() on shutdown to send a goodbye packet.

func StartAnnounce added in v0.22.157

func StartAnnounce(ctx context.Context, card *Card, port int) (*Announcer, error)

StartAnnounce registers `_clawtool._tcp.local` on the host's network interfaces, advertising `port` with TXT records that carry the Agent Card URL + peer_id + product version. The caller is typically the daemon's serve handler, called after the HTTP listener is up so the advertised port is bindable.

Idempotent: a second call returns the existing instance rather than registering a duplicate. Callers MUST hold the returned *Announcer for the daemon's lifetime; calling Close() on the announcer deregisters cleanly.

Context cancellation: the announcer doesn't own a goroutine (zeroconf.Server runs its own mainloop), so ctx is used only to derive a deregister-on-cancel goroutine. Passing the daemon's root context lets SIGTERM unwind the announce.

func (*Announcer) Close added in v0.22.157

func (a *Announcer) Close() error

Close deregisters the announce and stops the underlying mDNS server. Idempotent — a second call is a no-op so daemon shutdown paths can call it without remembering whether they already did. Returns nil when the announcer was never started (defensive — callers should still nil-check).

Lock order: per-instance closeMu is acquired in a SHORT critical section to flip the closed flag and snapshot the server pointer, then released BEFORE the longer-running Shutdown() call and the announceMu acquisition for singleton clearing. The ctx-cancel goroutine spawned by StartAnnounce can race a direct Close from the daemon shutdown path; this ordering prevents deadlock against a caller holding announceMu (e.g. test reset paths).

func (*Announcer) PeerID added in v0.22.157

func (a *Announcer) PeerID() string

PeerID returns the announcer's stable identifier. Used by the browser to filter the local daemon's own announcement out of the discovered-peer stream.

func (*Announcer) Port added in v0.22.157

func (a *Announcer) Port() int

Port returns the advertised port (the daemon's HTTP listener). Exposed for the test suite + diagnostic surfaces; runtime callers should read it from daemon.json instead.

type Browser added in v0.22.157

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

Browser is the running mDNS browse handle. Owns the entries channel + the discovered-peer goroutine that translates zeroconf service events into Registry.Register calls.

func StartBrowse added in v0.22.157

func StartBrowse(ctx context.Context, reg *Registry, selfPeerID string) (*Browser, error)

StartBrowse spins up a parallel zeroconf browse loop watching `_clawtool._tcp.local`. Every discovered ServiceEntry whose peer_id TXT field differs from `selfPeerID` is registered into `reg` with `source: "mdns"` and a metadata block carrying the parsed TXT fields (agent_card_url, version).

Returns immediately after the resolver is set up; the worker goroutine runs until Close() (or ctx cancel) tears it down. Idempotent: a second call returns the existing instance.

selfPeerID may be empty when the local daemon has no announcer (browse-only mode, e.g. a CLI inspection verb). In that case every discovered peer is registered.

func (*Browser) Close added in v0.22.157

func (b *Browser) Close() error

Close stops the browse loop and waits for the worker goroutine to exit. Idempotent — second call is a no-op so daemon shutdown paths can call it unconditionally. Same two-phase lock ordering as Announcer.Close — release the per-instance mutex before grabbing the package-level mutex so a caller holding browseMu (e.g. test reset) can't deadlock against the worker goroutine waiting on closeMu.

type Capabilities

type Capabilities struct {
	// Streaming: server-sent events for long-running tasks. We
	// have BIAM (TaskNotify) which is conceptually the same;
	// phase 2 wires the HTTP transport.
	Streaming bool `json:"streaming"`

	// PushNotifications: webhook delivery on task transitions.
	// Same primitive as TaskNotify but cross-network. Phase 3+.
	PushNotifications bool `json:"pushNotifications"`

	// StateTransitionHistory: peer can replay every task
	// state. BIAM stores envelope history; we'll expose it
	// when phase 2 ships the HTTP /tasks/{id} endpoint.
	StateTransitionHistory bool `json:"stateTransitionHistory"`
}

Capabilities is A2A's feature-flag block. We model only the flags clawtool currently cares about; A2A's spec allows additional vendor extensions.

type Card

type Card struct {
	// Name is the human-readable agent name shown in registries
	// and peer dashboards. We use clawtool's instance name when
	// the operator has set one; otherwise the bare project
	// name.
	Name string `json:"name"`

	// Description is one paragraph of plain text describing what
	// this agent does. Peers may render it in discovery UIs.
	Description string `json:"description"`

	// URL is the JSON-RPC endpoint base. Empty in phase 1
	// (Card-only mode); populated when phase 2 lands the HTTP
	// server.
	URL string `json:"url,omitempty"`

	// Version is the agent's product version (clawtool's
	// internal/version.Resolved()). NOT the A2A protocol version;
	// that's protocolVersion below.
	Version string `json:"version"`

	// ProtocolVersion is the A2A spec the card conforms to.
	// Follow upstream as it evolves; pinning here lets peers
	// negotiate.
	ProtocolVersion string `json:"protocolVersion"`

	// Capabilities is the feature flag block. We surface only
	// the streaming + push-notifications primitives clawtool
	// can actually serve today; future phases flip more on as
	// the implementation lands.
	Capabilities Capabilities `json:"capabilities"`

	// Skills enumerates the high-level abilities this agent
	// advertises. We don't dump every internal tool — that
	// would leak the operator's private surface and overflow
	// the card. Skills are *coarse* groupings (research,
	// code-edit, dispatch) the peer chooses from.
	Skills []Skill `json:"skills"`

	// DefaultInputModes / DefaultOutputModes — A2A's MIME-typed
	// I/O contract. clawtool speaks plain text + JSON-RPC; we
	// don't ship audio / image modes today.
	DefaultInputModes  []string `json:"defaultInputModes"`
	DefaultOutputModes []string `json:"defaultOutputModes"`

	// SecuritySchemes describes the auth modes this agent
	// accepts. Phase 1 advertises an empty schemes block (peers
	// can read the card but can't authenticate yet). Phase 2+
	// adds Bearer / OAuth schemes per ADR-024 §Threat model.
	SecuritySchemes map[string]SecurityScheme `json:"securitySchemes,omitempty"`

	// PublishedAt is when this card snapshot was generated.
	// Useful for caches / freshness checks. Always UTC.
	PublishedAt time.Time `json:"publishedAt"`
}

Card is the canonical A2A Agent Card (Schema v0.2.x, the stable LF-A2A snapshot as of 2026-04). Field names match the spec verbatim — JSON-RPC clients consuming the card MUST be able to parse it without translation.

func NewCard

func NewCard(opts CardOptions) Card

NewCard builds the Card snapshot for this clawtool instance. Pure function — fields come from CardOptions + version.Resolved() + a static skill list. Caller serializes via json.Marshal.

func (Card) MarshalIndented

func (c Card) MarshalIndented() ([]byte, error)

MarshalIndented is the human-readable form for CLI output. Two-space indent matches GitHub Actions' workflow YAML convention so an operator copy-pasting the output into a gist / issue gets a readable layout.

func (Card) MarshalJSON

func (c Card) MarshalJSON() ([]byte, error)

MarshalJSON serializes the card to compact JSON — what an HTTP handler would write to /.well-known/agent-card.json. We use MarshalIndent for the CLI surface (`clawtool a2a card`) so humans can read it; the server-side path uses bare json.Marshal.

type CardOptions

type CardOptions struct {
	// Name overrides the default ("clawtool"); empty keeps default.
	Name string
	// URL is the JSON-RPC endpoint. Empty until phase 2 lands.
	URL string
	// ExtraSkills appends to the canonical skill list. Empty
	// gives just the canonical set.
	ExtraSkills []Skill
}

CardOptions carries the per-instance fields we don't want hard-coded into NewCard so a future supervisor can customise per dispatch (e.g. emit different skills depending on which instance is calling).

type Inbox added in v0.22.36

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

Inbox is the per-peer message queue. One Inbox per registered peer; created lazily on first send. Methods are safe for concurrent calls — mu guards both the queue and the on-disk snapshot.

func (*Inbox) Drain added in v0.22.36

func (i *Inbox) Drain(peek bool) []Message

Drain returns every queued message and empties the inbox. Pass peek=true to read without consuming — the runtime's UserPromptSubmit hook uses peek to avoid losing messages if the recipient cancels the prompt.

func (*Inbox) Enqueue added in v0.22.36

func (i *Inbox) Enqueue(msg Message) Message

Enqueue appends `msg` to this inbox, capping to inboxCap and dropping the oldest if needed. Returns the persisted message (with assigned ID + timestamp when the caller didn't supply them). Idempotent on (FromPeer, Timestamp, Text) is NOT attempted — duplicate sends mean the sender retried; the recipient sees both.

type ListFilter added in v0.22.36

type ListFilter struct {
	Status  PeerStatus
	Path    string
	Backend string
	Circle  string
}

ListFilter narrows the result set returned by List. Empty fields are no-ops so callers can pass {Backend: "claude-code"} to see just claude peers.

type Message added in v0.22.36

type Message struct {
	ID            string      `json:"id"`
	Type          MessageType `json:"type"`
	FromPeer      string      `json:"from_peer"`
	ToPeer        string      `json:"to_peer,omitempty"` // omitted for broadcast
	Text          string      `json:"text"`
	CorrelationID string      `json:"correlation_id,omitempty"` // matches a prior query's ID
	Timestamp     time.Time   `json:"timestamp"`
}

Message is one envelope in the peer mesh.

type MessageType added in v0.22.36

type MessageType string

MessageType matches repowire's protocol/messages.py taxonomy. Locked at v0.22; new types are additive.

const (
	MsgQuery        MessageType = "query"        // expects a response
	MsgResponse     MessageType = "response"     // reply to a query (correlation_id required)
	MsgNotification MessageType = "notification" // fire-and-forget
	MsgBroadcast    MessageType = "broadcast"    // to all peers (to_peer ignored)
)

type Peer added in v0.22.36

type Peer struct {
	PeerID       string            `json:"peer_id"`
	DisplayName  string            `json:"display_name"`
	Path         string            `json:"path,omitempty"`
	Backend      string            `json:"backend"` // claude-code | codex | gemini | opencode | clawtool
	Circle       string            `json:"circle"`  // group name; defaults to tmux session or "default"
	Role         PeerRole          `json:"role"`
	Status       PeerStatus        `json:"status"`
	SessionID    string            `json:"session_id,omitempty"` // runtime-supplied session key (claude-code: hook payload .session_id)
	TmuxPane     string            `json:"tmux_pane,omitempty"`
	PID          int               `json:"pid,omitempty"`
	Metadata     map[string]string `json:"metadata,omitempty"`
	RegisteredAt time.Time         `json:"registered_at"`
	LastSeen     time.Time         `json:"last_seen"`
}

Peer is the single source of truth for one registered session. Field names are JSON-serialised verbatim so the wire shape (the `/v1/peers` endpoint) reflects the in-memory model directly.

type PeerRole added in v0.22.36

type PeerRole string

PeerRole differentiates dispatchers (orchestrators) from dispatchees (worker agents). Most peers are agents; an operator running multiple terminals manually flips one to orchestrator if they want it to coordinate the others.

const (
	RoleAgent        PeerRole = "agent"
	RoleOrchestrator PeerRole = "orchestrator"
)

type PeerStatus added in v0.22.36

type PeerStatus string

PeerStatus is the lifecycle marker every peer carries.

const (
	PeerOnline  PeerStatus = "online"
	PeerBusy    PeerStatus = "busy"
	PeerOffline PeerStatus = "offline"
)

type RegisterInput added in v0.22.36

type RegisterInput struct {
	DisplayName string            `json:"display_name"`
	Path        string            `json:"path,omitempty"`
	Backend     string            `json:"backend"`
	Circle      string            `json:"circle,omitempty"`
	Role        PeerRole          `json:"role,omitempty"`
	SessionID   string            `json:"session_id,omitempty"`
	TmuxPane    string            `json:"tmux_pane,omitempty"`
	PID         int               `json:"pid,omitempty"`
	Metadata    map[string]string `json:"metadata,omitempty"`
}

RegisterInput is the shape callers supply to Register. Mirrors the JSON body of POST /v1/peers/register so the HTTP handler is a thin marshaller.

type Registry added in v0.22.36

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

Registry is the process-wide peer table. One instance lives in the daemon for the lifetime of the process; constructed via NewRegistry which loads any persisted state.

func GetGlobal added in v0.22.36

func GetGlobal() *Registry

GetGlobal returns the process-wide registry, or nil when no daemon has set one. Read-side callers (CLI tools, MCP handlers) should nil-check; the returned value is safe for concurrent access.

func NewRegistry added in v0.22.36

func NewRegistry(statePath string) *Registry

NewRegistry constructs an empty registry, then attempts to load state from path. A missing / unreadable / corrupt file is non-fatal: we start with an empty table and log to stderr.

func (*Registry) Broadcast added in v0.22.36

func (r *Registry) Broadcast(msg Message) int

Broadcast enqueues `msg` into every currently-known peer's inbox (except the sender's own, identified by msg.FromPeer). Returns the count of recipients reached. Used by MsgBroadcast — one HTTP hit fans out to all live sessions.

func (*Registry) Deregister added in v0.22.36

func (r *Registry) Deregister(peerID string) (*Peer, error)

Deregister removes a peer outright. Used by SessionEnd hooks when the session is shutting down cleanly. Returns the removed peer (or nil) so callers can surface a "peer X went offline" event. Also drops the peer's inbox so deregistered sessions don't leave persisted mailboxes behind.

func (*Registry) DrainInbox added in v0.22.36

func (r *Registry) DrainInbox(peerID string, peek bool) []Message

DrainInbox returns the pending messages for peerID and clears them (or peeks, leaving them queued). Non-existent peers return an empty slice — the inbox is created lazily and an empty drain stays empty.

func (*Registry) Get added in v0.22.36

func (r *Registry) Get(peerID string) *Peer

Get returns one peer by ID, or nil when unknown. Pure read, no lazy-repair (the lazy sweep is List's job).

func (*Registry) Heartbeat added in v0.22.36

func (r *Registry) Heartbeat(peerID string, status PeerStatus) (*Peer, error)

Heartbeat refreshes a peer's last_seen + status. Returns nil-error / nil-peer when the peer_id is unknown; that's the "I just registered, then noticed my session ID was wrong" case — caller should re-register, not retry.

func (*Registry) List added in v0.22.36

func (r *Registry) List(filter ListFilter) []Peer

List returns every peer matching the filter. Lazy-repair runs inline: peers whose last_seen is older than HeartbeatStaleAfter flip to PeerOffline before the result is built; peers whose declared path no longer exists are dropped entirely. Sort order: online first, then by display_name lexicographic — so `clawtool a2a peers` reads top-down "currently active first".

func (*Registry) Register added in v0.22.36

func (r *Registry) Register(in RegisterInput) (*Peer, error)

Register adds a new peer (or refreshes an existing one with the same identity tuple) and returns the assigned peer_id. Idempotent: repeated calls with the same backend + path + tmux_pane + pubkey update the existing row's last_seen instead of creating a duplicate. Without this, every hook fire would multiply the peer table.

func (*Registry) Save added in v0.22.36

func (r *Registry) Save() error

Save persists the registry to its state path. Atomic via temp+rename so a crash mid-write doesn't leave a half-formed JSON. Idempotent — if dirty=false, no I/O happens.

func (*Registry) SaveAsync added in v0.22.39

func (r *Registry) SaveAsync()

SaveAsync runs Save on a background goroutine and tracks it on r.saves so test cleanup paths can drain in-flight writes via WaitForSaves before the state path's directory is removed. Use instead of `go r.Save()` from any handler that may execute under a t.TempDir-rooted state path.

func (*Registry) SendTo added in v0.22.36

func (r *Registry) SendTo(peerID string, msg Message) Message

SendTo enqueues `msg` into peerID's inbox. Returns the assigned message (with ID + timestamp). Caller must have validated peerID exists in the registry — the inbox creates lazily, so this would happily accept messages for a non-existent peer otherwise.

func (*Registry) WaitForSaves added in v0.22.39

func (r *Registry) WaitForSaves()

WaitForSaves blocks until every SaveAsync goroutine in flight has finished its atomicfile write. Tests using t.TempDir for the state path call this before letting the cleanup hook RemoveAll the dir; otherwise macOS's stricter unlinkat fails with "directory not empty" on the still-pending temp file.

type SecurityScheme

type SecurityScheme struct {
	Type        string `json:"type"`             // "http" | "oauth2" | "apiKey"
	Scheme      string `json:"scheme,omitempty"` // for http: "bearer" / "basic"
	Description string `json:"description,omitempty"`
}

SecurityScheme mirrors A2A's auth-scheme block. We expose only the fields clawtool's phase 2 will actually populate; A2A's full spec covers OAuth 2.1, mTLS, API key, etc.

type Skill

type Skill struct {
	ID          string   `json:"id"`
	Name        string   `json:"name"`
	Description string   `json:"description"`
	Tags        []string `json:"tags,omitempty"`
	// Examples are short prompts a peer can use to test the
	// skill. Optional. Keep them representative, not
	// exhaustive — the card is a contract, not a tutorial.
	Examples []string `json:"examples,omitempty"`
}

Skill is one coarse ability the agent advertises. A2A treats skills as the discovery primitive — a peer scanning a roster of cards looks at skill IDs to decide who can help.

Jump to

Keyboard shortcuts

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