Imported from FabricLabs/fabric (
AGENTS.md). Install upstream withnpx skills add FabricLabs/fabric. Copyright stays with the author.
Fabric Agents
See also DEVELOPERS.md (repo layout, tests) and docs/PRODUCTION.md (release gate).
Release posture
- Target:
0.1.0-RC1reference client — not a production-hardened VM claim (PUBLIC_API.md, AUDIT.md). - Gate:
npm run cion Node 24.15.0. Tag core before bumping Hub / http. - Do not paper over AUDIT “known gaps” or expand protocol surface for RC without owner go-ahead.
Fabric enables automated payments between node instances, which we can leverage for distributing load across multiple cores.
Fabric Agents are long-running services that can:
- hold identity (
agent.identity.address) - exchange messages with peers
- expose deterministic behavior through service lifecycle methods
- coordinate compute work and value transfer between nodes
Onion / directed forward (IP privacy)
Downstream apps that must hide origin IPs from destinations SHOULD use
P2P_FORWARD (0x45) via @fabric/core/functions/fabricOnion and
Peer#sendOnion(path, payload) — not mesh P2P_RELAY flood. See
docs/P2P_FORWARD.md.
Scope
This specification applies to:
- services that extend
types/service - local agents (single process)
- distributed agents (multiple processes, peers, or cores)
Agent Contract
An agent SHOULD implement the following interface:
- Constructor:
new Agent(settings = {}) - Lifecycle:
async start(),async stop() - Optional scheduler:
tick() - State: mutable service state under
this._state.content - Events: emits
debug,error, and domain-specific events
Recommended settings shape
{
function: () => {}, // optional unit of work
...otherSettings
}
Runtime Behavior
Common behavior expected from Fabric agents:
- initialize state in constructor
- validate required settings before startup
- emit traceable startup/shutdown logs
- isolate failures (worker/process error handlers)
- clean up resources on stop
Safety and Reliability
Agents SHOULD follow these operational rules:
- never assume remote input is trusted
- wrap async boundaries with error handling
- fail noisy (
errorevents) and recover where possible - avoid mutating shared state without clear ownership
- make start/stop idempotent when practical
Observability
At minimum, agents SHOULD:
- emit
debuglogs for lifecycle transitions - emit
errorlogs with enough context to diagnose failures - expose useful identifiers (for example, payment or identity address)
Example: Multi-Core Distributor
Reference implementation: examples/agents.js.
The example demonstrates common agent elements from this specification:
- extends
Service - keeps explicit internal state in
this._state.content - exposes
addressfrom identity - starts workers across available CPU cores via
worker_threads - handles
message,error, andexitevents - provides
start()andstop()lifecycle methods
Running examples/agents.js
- Help:
node examples/agents.js --help - CI / quick smoke:
npm run example:agents:smokeornode examples/agents.js --smoke(distributor + workers, no daemons) - Distributor demo (long-running):
npm run example:agents:distributor— producer loop until Ctrl+C - Regtest Bitcoin + distributor (no Lightning):
npm run example:agents:bitcoin— lighter “real” stack - Full stack:
node examples/agents.js— managed bitcoind + two CLN nodes + distributor + Lightning payments for priority queue
Use --work-ms=<n> or FABRIC_AGENTS_WORK_MS for per-job delay; FABRIC_AGENTS_BOOT_IDLE_MS caps the initial drain wait on the full and bitcoin-only paths.
Reusing a local regtest bitcoind on port 18443: the Bitcoin service probes cookie auth at ./stores/bitcoin-regtest/regtest/.cookie and, on macOS, ~/Library/Application Support/Electron/stores/bitcoin-regtest/regtest/.cookie. Set FABRIC_BITCOIN_COOKIE_FILE to your .cookie path if the node lives elsewhere. That avoids spawning a second bitcoind and hitting RPC bind errors.
agents.js: Bitcoin + Lightning integration
The full example runs a Bitcoin regtest node, Master Lightning node, and Alice Lightning node. Flow:
- Bitcoin — Start, mine 101 blocks if no spendable UTXOs, then mine one block every 10s.
- Master Lightning — Start, deposit 50% of spendable BTC if needed, confirm with 6 blocks.
- Alice Lightning — Start after 3s stagger; optionally clean datadir when
FABRIC_CLEAN_ALICE=1. - Channels — Master→Alice and Alice→Master, each funded with 50% of available Lightning funds, with push_msat for balanced liquidity.
Configuration: FABRIC_MNEMONIC for deterministic keys; ALICE_LIGHTNING_PORT (default 19735); MIN_CHANNEL_FUNDING_SATS (default 10000); MAX_ALICE_DEPOSIT_BTC (default 1) caps Alice's on-chain deposit to avoid "Fee exceeds maximum" when the wallet has many UTXOs.
Lightning: multi-node setup
When running multiple Lightning nodes (Master + Alice) against one bitcoind:
disablePlugins: ['cln-grpc']— Required for secondary nodes to avoid conflicts.- Stagger startup — 3s between Master and Alice to reduce bitcoind contention.
- Clean datadir — Run with
FABRIC_CLEAN_ALICE=1if Alice fails with ECONNREFUSED or socket errors (stale state from prior runs).
Lightning: troubleshooting
The Lightning service uses timeouts and retries to avoid hangs:
- RPC timeout — 30s default; if
getinfoblocks (e.g. during chain sync), the call fails and retries. - Socket wait — Up to 60s polling for the RPC socket; stale sockets are removed before spawning.
- Check
[FABRIC:LIGHTNING] [ERROR]stderr from the lightningd child for startup failures.
Testing instructions
- Basic test:
npm test
Example
'use strict';
const os = require('os');
const cluster = require('cluster');
const Service = require('../types/service');
const numberOfCores = os.cpus().length;
class Distributor extends Service {
constructor (settings = {}) {
super(settings);
this._state.content = {
cores: [],
function: (settings.function) ? settings.function.bind({}) : (() => {}).bind({})
};
}
get address () {
return this.identity.address;
}
async start () {
this.emit('debug', 'Starting Distributor...');
for (let i = 0; i < numberOfCores; i++) {
const instance = this.settings.function.bind({});
const core = new cluster.Worker(instance);
core.on('message', (message) => this.emit('debug', 'Core message:', message));
core.on('error', (error) => this.emit('error', 'Core error:', error));
core.on('exit', (code, signal) => this.emit('debug', 'Core exited:', code, signal));
core.send('start');
}
this.emit('debug', `Distributor started. Fund this address: ${this.address}`);
return this;
}
}
Release verification
npm run ci # NODE_ENV=test mocha --recursive tests
Operator and marketing context: docs/PRODUCTION.md, docs/MARKETING_OVERVIEW.md.
Authoring Checklist
Before merging a new Fabric agent:
- constructor initializes explicit state shape
- lifecycle methods are present and tested
- startup failure paths are handled
- logs/events are sufficient for diagnosis
- example usage is documented (inline or in
examples/)
Author Style
- do not abbreviate in APIs;
documentIdshould bedocumentIdentifier
JSDoc (for npm run make:api)
jsdoc2md / Closure parser reject TypeScript optional property syntax inside
tags. When documenting optional option bags:
// Bad — breaks make:api
/** @param {{ genesis?: object }} [opts] */
// Good
/**
* @param {Object} [opts]
* @param {Object} [opts.genesis] ARC genesis (primitives.opcodes, …)
*/
Also: @type {T} must not carry a trailing description; put the prose in the
block above the @type line. See DEVELOPERS.md (Development
workflow → API reference).
Avoid Global clutter: file-level prose must use @fileoverview (or
@module) so it does not attach to the next const/require. Object-literal
settings fields should use // comments, not /** @type */ blocks (those
become Global symbols). Mark module-private helpers/constants @private so
they do not appear on docs/global.html / API.md Globals.