Class PeerNode

java.lang.Object
ai.badmonkey.agentspaces.peering.node.PeerNode
All Implemented Interfaces:
AutoCloseable

public final class PeerNode extends Object implements AutoCloseable
A peer: one process's presence in the AgentSpaces fabric (spec §5). A node holds the peer identity, the configured transports, one GroupRuntime per joined group, and the frame plumbing between them: every outbound frame is a CBOR envelope carrying an Ed25519 signature, or traveling unsigned on a channel the transport authenticated for exactly its sender (spec §5.6), and every inbound frame passes that same check before any handler sees it.

Progress is tick-driven: tick() runs one membership probe round and one self-advertisement refresh per group, plus one anti-entropy round per group whenever that group's gossip period has elapsed on the node's clock (spec §5.3). Tests call it explicitly with a test clock; wall-clock deployments call startTicking.

  • Field Details

    • DEFAULT_TICK_PERIOD

      public static final Duration DEFAULT_TICK_PERIOD
      The tick cadence assumed when a host drives tick() by hand rather than through startTicking(Duration), and the starter's default for agentspaces.tick-millis.
  • Method Details

    • builder

      public static PeerNode.Builder builder(PeerIdentity identity)
      Starts building a node.
      Parameters:
      identity - the peer identity
      Returns:
      the builder
    • identity

      public PeerIdentity identity()
      Returns the peer's identity.
    • onCredentialHints

      public void onCredentialHints(BiConsumer<PeerId, Map<String,String>> listener)
      Registers a credential-hint listener on a built node; the same contract as PeerNode.Builder.onCredentialHints(BiConsumer), for wiring that learns of the listener after the node exists (the Spring starter's authorizer bean).
      Parameters:
      listener - receives the peer and its current hint map
    • credentialHint

      public void credentialHint(String key, String value)
      Sets one of this node's own credential hints (TODO-EFG §4): the value is carried under key in every group's next signed self-advertisement, so every member's hint listener sees it within one gossip period. Refreshing a token is calling this again with the new value. Hints are bounded only by the advertisement they ride in; keep them small (an OIDC token is typically under 2 KiB). A per-group join hint of the same key takes precedence for that group.
      Parameters:
      key - the hint key, e.g. JoinCredentials.OIDC_HINT_KEY
      value - the hint value
    • removeCredentialHint

      public void removeCredentialHint(String key)
      Removes one of this node's own credential hints; the next self-advertisement no longer carries it.
      Parameters:
      key - the hint key
    • requiresAttestation

      public boolean requiresAttestation()
      Whether this node requires transport attestation of every frame's sender (the CA channel mode, spec §5.6). Exposed so a deployment can assert the posture it configured actually reached the node.
      Returns:
      whether attestation is required
    • transportSchemes

      public Set<String> transportSchemes()
      The transport schemes registered on this node, for dialing and for the endpoints it advertises. A node in the attested mode registers only transports that can attest, so an unattestable scheme's absence here is what keeps an honest peer from dialing in on a channel that would get it evicted.
      Returns:
      the registered schemes
    • peerId

      public PeerId peerId()
    • signedFramesSent

      public long signedFramesSent()
      Returns how many fully signed frames this node has sent.
    • bareFramesSent

      public long bareFramesSent()
      Returns how many channel-attested (unsigned) frames this node has sent.
    • framesDropped

      public long framesDropped()
      Returns how many inbound frames the wire codec refused outright (spec §9): failed signature, half-signed, unknown wire version, or garbage. Such frames name no verified sender, so they count here and strike nobody.
    • channelModes

      public Map<PeerId,String> channelModes()
      Returns the channel-authentication mode per connected peer, for the console and diagnostics: "attested" when frames to that peer ride an authenticated channel unsigned, "signed" otherwise.
      Returns:
      peer to mode, a snapshot
    • listen

      public void listen(Transport transport, String bindAddress) throws IOException
      Registers a transport and starts listening on it. The bind address is also advertised to other peers.
      Parameters:
      transport - the transport
      bindAddress - the transport-specific bind (and advertised) address
      Throws:
      IOException - if the address cannot be bound
    • listen

      public void listen(Transport transport, String bindAddress, int priority) throws IOException
      Registers a transport, starts listening on it, and advertises the bind address at an explicit priority (spec §5.5): dialers try a peer's endpoints in ascending priority, so a node advertising quic at 0 and tcp at 1 is reached over QUIC wherever the dialer speaks it. The two-argument overload assigns priorities in listen order.
      Parameters:
      transport - the transport
      bindAddress - the transport-specific bind (and advertised) address
      priority - the advertised priority; lower dials first
      Throws:
      IOException - if the address cannot be bound
    • transport

      public void transport(Transport transport)
      Registers a transport for outbound dialing only, without listening or advertising an endpoint. This is the NAT-restricted posture (spec §5.4): the peer dials out to seeds and members, its self-advertisement carries no endpoints, and peers that need to reach it route through a RELAY-role member instead.
      Parameters:
      transport - the transport
    • joinGroup

      public GroupRuntime joinGroup(GroupAdvertisement groupAd, GroupMembership.Config membershipConfig, List<PeerAdvertisement.Endpoint> seeds)
      Joins a locally configured, literal-id group and returns its runtime.

      This overload performs no self-certification check: the advertisement is this node's own configuration, handed in by the operator, so the network never had a chance to swap its policy. It is the right call for fleets whose members all ship the same configured group, and for tests. A group whose advertisement must be learned — fetched from a seed by GroupID, or received in any other way from the network — must instead go through joinGroup(SignedGroupAdvertisement, GroupMembership.Config, List) or joinGroup(GroupId, GroupMembership.Config, List, Duration), which verify the founding document (spec §4.4, §5.1). A literal-id group is never served to a newcomer asking by GroupID.

      Parameters:
      groupAd - the group's configured advertisement
      membershipConfig - membership tuning
      seeds - bootstrap endpoints of any existing members; empty for the first member
      Returns:
      the group runtime
    • joinGroup

      public GroupRuntime joinGroup(SignedGroupAdvertisement founding, GroupMembership.Config membershipConfig, List<PeerAdvertisement.Endpoint> seeds)
      Joins a self-certifying group with its signed founding advertisement (spec §4.4, §5.1), verifying it first: the founder's key must hash to the issuer, the signature must verify over the founding fields, and the GroupID must equal the hash of the founding document. A document that fails is refused and nothing is joined. Members joined this way serve the document to newcomers that ask by GroupID.
      Parameters:
      founding - the signed founding advertisement
      membershipConfig - membership tuning
      seeds - bootstrap endpoints of existing members
      Returns:
      the group runtime
      Throws:
      IllegalArgumentException - when the founding document does not verify
    • joinGroup

      public GroupRuntime joinGroup(SignedGroupAdvertisement founding, GroupMembership.Config membershipConfig, List<PeerAdvertisement.Endpoint> seeds, Map<String,String> membershipHints, MembershipValidator validator)
      Joins a self-certifying group, verifying its founding advertisement and presenting membership credentials and a POLICY validator as in joinGroup(GroupAdvertisement, GroupMembership.Config, List, Map, MembershipValidator).
      Parameters:
      founding - the signed founding advertisement
      membershipConfig - membership tuning
      seeds - bootstrap endpoints of existing members
      membershipHints - resource hints for our self-ad (join credentials)
      validator - POLICY admission validator, or null
      Returns:
      the group runtime
      Throws:
      IllegalArgumentException - when the founding document does not verify
    • joinGroup

      public GroupRuntime joinGroup(GroupId groupId, GroupMembership.Config membershipConfig, List<PeerAdvertisement.Endpoint> seeds, Duration timeout) throws TimeoutException
      Joins a group knowing only its GroupID and seed endpoints (spec §10.1 join: "aspace://<groupID>"): asks each seed for the founding advertisement (GROUP_AD_WANT), waits for the first answer that verifies as the self-certifying founding document of exactly that GroupID (spec §4.4, §5.1), and then joins with it as joinGroup(SignedGroupAdvertisement, GroupMembership.Config, List) would. Answers that fail verification — a different policy under the same id, a forged signature — are ignored, so a hostile seed can delay the join but never substitute the group; only honest seeds that themselves joined with the founding document ever answer.
      Parameters:
      groupId - the self-certifying GroupID to join
      membershipConfig - membership tuning
      seeds - seed endpoints to ask and then introduce ourselves to
      timeout - wall-clock bound on the fetch
      Returns:
      the group runtime
      Throws:
      TimeoutException - when no seed served a verified founding advertisement within the timeout
      IllegalStateException - when the group is already joined or a fetch for it is already in progress
    • joinGroup

      public GroupRuntime joinGroup(GroupAdvertisement groupAd, GroupMembership.Config membershipConfig, List<PeerAdvertisement.Endpoint> seeds, Map<String,String> membershipHints, MembershipValidator validator)
      Joins a group, presenting membership credentials and (for POLICY groups) an admission validator (spec §5.1). The membershipHints travel in this peer's signed self-advertisement so other members can admit it: an INVITE group expects JoinCredentials.HINT_KEY to carry a founder-issued credential. The validator, when non-null, decides admission of other peers into a POLICY group.
      Parameters:
      groupAd - the group's founding advertisement
      membershipConfig - membership timing configuration
      seeds - seed endpoints to introduce ourselves to
      membershipHints - resource hints for our self-ad (join credentials)
      validator - POLICY admission validator, or null
      Returns:
      the joined group's runtime
    • group

      public Optional<GroupRuntime> group(GroupId groupId)
      Returns a joined group's runtime.
      Parameters:
      groupId - the group
      Returns:
      the runtime, when joined
    • tick

      public void tick()
      Runs one protocol round for every joined group: refresh the leased self-advertisement, probe membership, and — once per gossip period of the node's clock (spec §5.3, GossipParameters.period) — reconcile state with one partner. Rumor forwarding and probes are not paced by the period. Also announces on the bootstrap beacon, when one is configured, once per beacon interval.
    • startTicking

      public void startTicking(Duration period)
      Starts a background ticker for wall-clock deployments.
      Parameters:
      period - the tick period
    • tickPeriod

      public Optional<Duration> tickPeriod()
      The wall-clock period between tick() calls, when this node knows it: the period startTicking(Duration) was called with. Empty when the host drives tick() by hand, because then only the host knows the cadence, and a test advancing a TestClock a simulated second per tick is not ticking at any wall-clock rate at all.

      Layers above peering that express their semantics in ticks read this to convert to wall time, and must keep their own declared cadence when it is empty rather than substituting a guess; see CapabilityProvider.driverCadence (QA3 A3-4).

      Returns:
      the cadence ticks arrive at, or empty when hand-driven
    • close

      public void close()
      Specified by:
      close in interface AutoCloseable