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
- func CircleKeyMatches(expected, presented string) bool
- func CircleKeyPath() string
- func ClearCircleKey() error
- func DefaultStatePath() string
- func GenerateCircleKey() (string, error)
- func LoadCircleKey() (string, error)
- func PeersStateDir() string
- func SaveCircleKey(key string) error
- func SetGlobal(r *Registry)
- func WaitForBrowseSettle(d time.Duration)
- type Announcer
- type Browser
- type Capabilities
- type Card
- type CardOptions
- type Inbox
- type ListFilter
- type Message
- type MessageType
- type Peer
- type PeerRole
- type PeerStatus
- type RegisterInput
- type Registry
- func (r *Registry) Broadcast(msg Message) int
- func (r *Registry) Deregister(peerID string) (*Peer, error)
- func (r *Registry) DrainInbox(peerID string, peek bool) []Message
- func (r *Registry) Get(peerID string) *Peer
- func (r *Registry) Heartbeat(peerID string, status PeerStatus) (*Peer, error)
- func (r *Registry) List(filter ListFilter) []Peer
- func (r *Registry) Register(in RegisterInput) (*Peer, error)
- func (r *Registry) Save() error
- func (r *Registry) SaveAsync()
- func (r *Registry) SendTo(peerID string, msg Message) Message
- func (r *Registry) WaitForSaves()
- type SecurityScheme
- type Skill
Constants ¶
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.
const CurrentProtocolVersion = "0.2.0"
CurrentProtocolVersion is the A2A spec snapshot we conform to. Bump in lockstep with upstream as their stable snapshots advance.
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).
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.
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
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
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
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
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
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
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
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).
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
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
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 ¶
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 ¶
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
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
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.
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
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
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
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
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
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
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
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.