fa_hub_client library
DAP/1 hub client for flutter_agent_harness.
Signed WebSocket messaging (channels, DMs, presence) with end-to-end
encrypted payloads — the hub only ever sees ciphertext. Shaped as an
upstreamable PR for IstiN/flutter_agent_harness (lib/src/hub/ there).
Classes
- AgentInfo
- Peer directory entry returned by whois/presence.
- AgentMessage
- One message between two agents (upstream shape).
- ChannelKeys
- One channel's X25519 keypair (base64).
- ChannelStore
-
Runtime channel-key state for one client: seeded from the shared
channels file, mutated by auto-keygen and accepted invites, persisted
read-modify-write. The hub never sees any of these keys (only public
keys via
join). - DapSettings
-
Optional persisted settings:
~/.dap/config.json(all fields optional). - FahPlugin
-
Base interface for a
fahplugin / package extension. - HubClient
- HubConfig
- HubIdentity
- HubMessagingRepository
- HubPlugin
- HubStatus
-
Connection snapshot for the
dap_statustool: who we are, where we are connected, what we can send to, and how the handshake is going. - InboundMessage
-
One inbound
msgframe: decrypted when a key is available. - MailboxEntry
- One entry in the messaging-fabric directory (upstream shape).
- MessagingRepository
- Isolated messaging backend for agent inboxes (upstream shape).
- PendingInvite
-
A pending by-name invite in the machine-shared
~/.dap/config.json(invites: [{name, channel}]): armed bydap_invite <name>for a user not yet on the hub, removed once the chankey DM was delivered. - PendingInvites
-
Pending by-name invites —
dap_invite <name>against a user not yet on the hub: persisted in the machine-shared~/.dap/config.jsonunderinvites: [{name, channel}], delivered automatically once the name appears online. One presence query per interval (~15 s) plus an immediate check at arm time and after every welcome (restart redelivery). Mirrors the omp-extension pending-invite behavior. - PluginContext
- Context passed to FahPlugin.register.
- PluginIO
- IO surface exposed to plugins for writing to the terminal.
Enums
- DapSecretSource
- Where resolveDapClientSecret found the dial credential.
Constants
- defaultDapUrl → const String
- envChannelsFile → const String
- envClientSecret → const String
- envConfigFile → const String
- envMasterSecret → const String
-
Frozen cross-adapter text for a hub bearer rejection (HTTP 401 before
the websocket upgrade) — byte-identical in every DAP adapter; the hub
answers
unauthorizedfor a missing/unknown secret.
Functions
-
canonicalJson(
Object? value) → String -
Recursively key-sorted, whitespace-free JSON encoding of
value. -
dapHostOf(
String url) → String -
The hub address for paste-ready connect lines: scheme and trailing
/wsstripped (ws://h:1/ws→h:1). -
decryptPayload(
{required SimpleKeyPair recipientDhKeyPair, required SimplePublicKey senderDhPubkey, required String frameId, required String aadTarget, required String ciphertextB64}) → Future< String> -
Decrypts a payload produced by encryptPayload.
aadTargetmust equal the sender's. Throws on wrong key or tampered ciphertext. -
defaultDapConfigFile(
[String? home, Map< String, String> ? environment]) → String -
~/.dap/config.jsonpath — the single authority every reader/writer of that file goes through (readDapConfig, persistDapConfig, resolveDapSettings).DAP_CONFIG_FILE(fromenvironment, defaultPlatform.environment) wins outright;homeoverrides~(test seam). -
defaultDapKeyPath(
String? name, String home) → String -
Default identity file, derived from the agent name (or hostname):
~/.dap/keys/fah/<sanitized>.key. -
defaultHome(
[Map< String, String> environment = const {}]) → String -
~on POSIX and Windows alike (dart:io has no homedir). -
encryptPayload(
{required SimpleKeyPair senderDhKeyPair, required SimplePublicKey recipientDhPubkey, required String frameId, required String aadTarget, required String plaintext}) → Future< String> -
Encrypts
plaintextfor the holder of the DH private key matchingrecipientDhPubkey.aadTargetis the channel name (channel send) or recipient agentId (DM). -
hexEncode(
List< int> bytes) → String -
hkdfSha256(
{required List< int> ikm, required List<int> salt, required List<int> info, required int length}) → Future<Uint8List> - HKDF-SHA256 extract+expand per RFC 5869.
-
loadChannelKeys(
String file) → Future< Map< String, ChannelKeys> > - Channels file → keypairs. Missing or invalid file = no channels; only entries with both keys survive.
-
newChannelKeypair(
) → Future< ChannelKeys> - Fresh channel X25519 keypair — generated by the channel's first user.
-
newFrameId(
) → String - Opaque unique frame id, uuid-v4 shaped.
-
normalizeDapHost(
String host) → String -
dap_connect host normalization: no scheme →
ws://, no path →/ws(hub.example.com→ws://hub.example.com/ws,hub:8787→ws://hub:8787/ws, an explicitws(s)://…/pathis kept as-is). -
parseChankeyInvite(
String text) → ({String channel, String priv, String pub})? -
A DM whose decrypted text is exactly a channel-invite payload
{"t":"chankey","channel","pub","priv"}. Regular chat that happens to start with{still fails the shape check and returns null. -
persistChannelKeys(
String file, String name, ChannelKeys keys) → Future< void> - Read-modify-write: keeps the other channels; creates the parent dir (e.g. ~/.dap) on demand.
-
persistDapConfig(
{String? url, String? name, List< String> ? channels, List<PendingInvite> ? invites, String? clientSecret, bool clearClientSecret = false, String? file}) → Future<void> -
Read-modify-write of
~/.dap/config.json(dap_connect persistence): mergesurl/name/channels/clientSecret;clearClientSecretremoves a stale cache entry (401 recovery — a secret the hub rejected must not win precedence over the master secret on later launches). The default-room list only grows (a union — rooms are never un-remembered).invitesis the authoritative pending-invite list — an empty list removes them all (delivered entries are dropped by the caller).fileis injectable for tests. Auto-join on later launches flows through the shared channels file (the store joins every channel it has keys for). -
randomHex(
int nChars) → String -
Random lowercase hex string of
nCharscharacters (nonce, ids). -
readDapConfig(
String file) → Map< String, dynamic> -
Reads
~/.dap/config.json; a missing or invalid file counts as absent. -
readPendingInvites(
String file) → List< PendingInvite> -
The
inviteslist from~/.dap/config.json; a missing or non-array key counts as empty (back-compat with files written before invites existed), malformed entries are skipped. -
resolveDapClientSecret(
{Map< String, String> environment = const {}, Map<String, dynamic> ? config}) → ({bool enroll, String? master, DapSecretSource source, String? token}) -
Hub dial credential per the enrollment contract:
DAP_CLIENT_SECRET(env) >clientSecretfrom~/.dap/config.json(config) >DAP_MASTER_SECRET(env — enroll-mode: the connection enrolls once and the hub-issued secret replaces it).token: nulldials anyway — the hub answers 401 and the client surfaces the frozen enrollment hint. -
resolveDapSettings(
{HubConfig config = const HubConfig(), Map< String, String> environment = const {}, String? home}) → DapSettings -
Resolves the effective settings (see library doc for precedence).
configis the already env-mergedhub:section (HubConfig.fromMap). -
signFrame(
Map< String, dynamic> frame, SimpleKeyPair keyPair) → Future<String> -
Signs
frame(withoutsigfield) and returns the b64 signature. -
signingPayload(
Map< String, dynamic> frame) → Future<String> -
The DAP/1 signing payload for
frame(which must not yet containsig). -
verifyFrame(
Map< String, dynamic> frame, String sigB64, SimplePublicKey publicKey) → Future<bool> -
Verifies a DAP/1 signature over
frame(withoutsigfield).
Typedefs
-
DapConnection
= ({String agentId, List<
String> channels, String? name, bool ok, String url}) - dap_connect result: the connection now in force (the new welcome's agentId, the normalized url, the display name, every joinable room).
-
ExternalSteeringSource
= Future<
List< Function()AgentMessage> > -
The
Agent.externalSteeringSourceseam: called at every turn boundary (before the first turn and after each one); drained BEFORE the in-process steering queue. Contract: must not throw; return an empty list when nothing arrived. - InviteResult = ({String channel, String? connectLine, String? error, bool ok, bool pending, String to})
-
dap_invite result:
ok: false, erroris an honest failure (not connected, ambiguous name, DM failure); otherwise the chankey DM went out immediately (pending: false) or the invite was armed for a user not yet online (pending: true— delivered automatically when they connect;connectLineis the paste-ready line for the invited user). -
SlashCommand
= Future<
void> Function(List<String> args) - A slash-command handler registered by a plugin (upstream shape).
Exceptions / Errors
- HubError
-
A hub
errorframe.