AgenNet is an experimental Agent-native coordination substrate built on existing network transports. v0.2.0-preview.4 is a Developer Preview — physical acceptance pending. It proves dynamic Capability discovery, signed bilateral Contracts, scoped Artifact access, independent verification, and evidence-gated acceptance through real multi-process and mTLS paths; it does not yet claim completed physical multi-device validation.
Use the Chinese or
English checksum-verifying installation guide.
macOS arm64/x86_64 and Linux arm64/x86_64 are supported. Windows users run the
Linux build inside WSL2; native Windows is unsupported. Agent operators can use
the fixed node bootstrap guide or the
packaged agenet-node-bootstrap Skill. Every Invitation and passphrase stays
inside the human's local controlling TTY.
AgenNet's North Star is to enable authorized coordination among participating Agents and network-reachable resources without requiring a global operator, then grow that network into an Agent Society capable of organizing specialized intelligence, balancing resources, managing conflicts, and helping people pursue complex goals. We believe such a society may become a practical path toward collective AGI. This is a long-term research hypothesis, not a claim about the v0.2 implementation.
The living vision—including horizontal Agent links, vertical knowledge and
institutional inheritance, an Agent Library, School, organizations,
maintenance, public-safety mechanisms, and quality-of-service transport—is in
docs/vision/agent-society.md.
The package is now version 0.2.0-preview.4. New v0.2 objects use the explicit
agenet.kernel.v0.2 kernel version. agenet.kernel.v0.1 remains identifiable
only for migration diagnostics; it is not an implicit compatibility mode and
does not authorize a v0.2 effect endpoint. The v0.2 wire envelope carries a
Root-to-Authority-to-Node credential chain, validates its Domain, scope,
profile, role, issuer, and lifetime before deserializing signed business
payload bytes, and rejects direct-root v0.1 credentials with
MigrationRequiredV1Credential.
There are no external v0.1 consumers in this repository's supported release surface. The loopback demo now provisions fresh v0.2 chains on every run. Retained v0.1 credential state is read only for migration diagnosis and must be regenerated; there is no v0.1 credential issuance API or v0.2 effect-path fallback.
The v0.2 baseline pins the TLS server, certificate, encryption, protected
input, HMAC, network-prefix, platform-directory, clock, and macOS plist
dependencies required by the subsequent bootstrap tasks. The review record,
including licenses, transitive footprint, and removal boundaries, is in
docs/security/dependency-review-v0.2.md.
Task 9 introduces a deliberately versioned local persistence boundary; it is
not a promise that these schemas or phase choices will never change.
agenet.node-config schema 2 is a bounded, deny-unknown-fields JSON document.
It carries the Domain, bootstrap profile, network boundary, Directory seeds,
Authority endpoint, and revocation endpoint. Private-overlay endpoints are
exact IP-literal HTTPS origins with no credentials, query, fragment, DNS name,
or non-root path. Plain HTTP is accepted only when the persisted boundary is
explicitly loopback.
Current-host roots are resolved through the directories crate. macOS uses
~/Library/Application Support/AgenNet/; Linux uses
${XDG_CONFIG_HOME:-~/.config}/agenet/ and
${XDG_STATE_HOME:-~/.local/state}/agenet/. Config, credential chain, Ed25519
signing key, TLS certificate/key/CA, service metadata, revocation cache,
journal, and process lock have separate versioned names. Managed directories
must be current-user 0700 non-symlink directories; owner-only files must be
regular current-user 0600 files and are opened with no-follow, nonblocking,
bounded reads. Startup material is returned only after credential
Root/domain/role/profile/time, signing-key, TLS chain/key, NodeId, exact IP SAN,
and certificate-time validation succeeds. Each Directory seed is the exact
versioned pair { endpoint, node_id }; URL-only legacy seeds fail closed.
The credential, signing key, certificate, TLS key, and CA are intentionally
separate files, not a claimed multi-file transaction. A writer must hold the
bootstrap state lock, publish and validate the complete set, and only then
append CredentialIssued. A crash may leave a partial set, but restart loads
every file and fails closed instead of deleting credentials or guessing which
file won. Task 10 owns command-level retry/reconciliation of that incomplete
set; Task 9 provides only the validated persisted-bundle boundary.
The local journal records this forward path:
Absent → BinaryInstalled → ReadyForEnrollment → CredentialIssued
→ ServicePrepared → Registered → Healthy
The only backward compensation is ServicePrepared → CredentialIssued; it
removes no credential material. Task 10 never records ServicePrepared, because
Task 11 must first publish a real service artifact. Leave moves any installed,
non-Left phase to Left. Every record contains a unique operation
ID, sequence, previous hash, transition, and checksum beneath a separate
versioned/checksummed header. Exact operation replay is idempotent; changed
reuse, illegal transitions, incomplete lines, unknown versions, corruption,
and bounds violations fail closed. One owner-only nonblocking process lock is
held for the store lifetime. An uncertain durable append poisons further
mutation until restart; atomic replacement failures after publish are likewise
reported as uncertain so restart can reconcile the visible final file.
Journal schema 2 encodes this order and rejects schema 1 rather than
reinterpreting its contradictory phase semantics. The journal is provisional
newline-framed JSONL, not binary framing. Its lock,
create/open, replay, header sync, and later appends remain anchored to one
verified owner-only directory descriptor and one journal descriptor, so a
pathname replacement cannot redirect publication between validation and sync.
Temporary private-material files are guarded from creation through publication:
any metadata, write, flush, or file-sync error unlinks the temporary name via
the pinned parent descriptor. Successful rename/link publication disarms that
cleanup before the parent-directory sync, so an uncertain post-publish sync
error never deletes the final file. A process crash can still leave a 0600
temporary inode; automatic wildcard cleanup is intentionally unsupported.
New journal creation exposes its owner-only final name before writing and syncing the versioned header. If header write, file sync, or parent-directory sync is uncertain, opening the store fails and the file is retained. Restart accepts only a complete valid header and otherwise fails closed; explicit operator/Task 10 repair is required. AgenNet does not automatically delete the file because a complete header may already be durable even when sync reported an error.
After domain init or node join has durably reached CredentialIssued, the
operator may explicitly run:
agenet node start
agenet node status --output json
agenet node stop
node start atomically publishes a real user-service definition, records
ServicePrepared, and only then asks the login-scoped service manager to start
it. macOS uses
~/Library/LaunchAgents/org.nexa-language.agenet.plist; Linux uses
~/.config/systemd/user/agenet.service. Every ancestor of that absolute path
is opened without following symlinks and must be root/current-user owned and
not group/world writable. An activation failure rolls the journal back to
CredentialIssued only after the platform manager verifies the process is
stopped and the exact definition is absent. Otherwise it retains
ServicePrepared, returns ServiceRollbackIncomplete, and keeps all
credential material. A later start republishes and reconciles an uncertain
post-delete state. Repeated start and stop operations do not advance to
Registered or Healthy.
The service runs the internal bootstrap supervisor. It strictly loads
the persisted config, Root/Authority/Node credential chain, signing key, peer
TLS identity, and schema-2 journal; holds the single-instance journal lock; and
exits gracefully on termination. Task 12 attaches the v0.2 peer runtime to this
same entrypoint. It loads the current revocation cache (refreshing through the
pinned Authority CA if necessary), derives handlers only from verified signed
roles, binds the exact configured address, performs an mTLS health probe, and
registers signed provider manifests before publishing an owner-only readiness
artifact. Registration/startup failure withdraws readiness and reaps the
listener. runtime_ready means this base peer runtime is listening under
current policy. Production domain init issues the founding node exactly the
signed Directory and Requester roles with an empty provider capability
ceiling and creates a separate owner-only local-control token. When both are
present, the runtime merges the Requester's signed Artifact route into the
private-overlay mTLS peer listener and starts a separate dynamic loopback-only
pursuit listener. Its owner-only ready record contains a local endpoint and
process generation, never the token. Its unauthenticated /healthz is
loopback-only and returns only the Node ID, process generation, and readiness
booleans. Every production peer /healthz is mTLS-only and returns only its
public Node ID, process generation, runtime readiness, and current revocation
boolean. Evidence collection self-probes that exact configured peer endpoint
on both A and B with the validated local certificate/key/CA bundle, requires an
exact TLS Node/process match, and independently requires the platform user
service to report a running process. Device A also requires its local-control
health to match ready and service metadata. A token without the signed role
fails closed; a signed role without the token leaves the Directory available
but disables pursuits. Provider enrollment creates no local-control token.
Bootstrap profile is never used as an authorization role or capability grant.
A verified founding Directory additionally loads a typed founding-only Authority runtime from the existing owner-only Authority credential/signing key, CA certificate/key, invitation state, enrollment-result directory, and revocation Authority state. It never opens the Domain Root private keystore. The supervisor signs a short-lived exact-IP server leaf, self-checks separate server-auth TLS enrollment and revocation listeners, keeps revocation snapshots fresh with the online Authority key, and cancels all listeners if any required surface fails. Non-Directory credentials never load these Authority files.
These services persist only after the owning user logs in. AgenNet does not use
sudo, install a system service, enable Linux linger, embed an environment
file, or put secrets in the service definition.
The demo runs four independent processes on dynamic loopback ports:
- Directory/Router
- Requester Agent
- Source Metrics Executor
- Independent Source Metrics Verifier
The first real Capability is source.metrics.v1: it computes a source Artifact's SHA-256 digest, byte count, line count, and non-empty line count. The Verifier independently recomputes the same metrics. Delivery does not become Accepted until verification succeeds.
The demo now uses Authority-CA mutual TLS for every peer hop, exact-IP SANs,
NodeId-bound signed envelopes, current revocation snapshots, and dynamically
signed Directory results. It probes 127.0.0.2 through 127.0.0.5; hosts that
do not expose those aliases fall back to distinct ports on 127.0.0.1. The
summary labels these as simulated_loopback_aliases_mtls or
loopback_ports_mtls beneath loopback_harness. Neither is physical
multi-machine evidence. It does
not validate distributed failover, quota accounting, arbitrary code
sandboxing, or Internet-scale discovery.
cargo run -- demo \
--env-file /path/to/model.env \
--artifact fixtures/sample.rsThe environment file is read in place. It is never copied, logged, or committed.
For a production founding node on a private overlay, the operator submits the same source-metrics flow through the local Requester boundary:
agenet pursuit run \
--env-file /path/to/model.env \
--artifact fixtures/sample.rs \
--output jsonThis command accepts no Directory, Executor, or Verifier endpoint. It retains
only the allowlisted OPENAI_BASE_URL, OPENAI_API_KEY, and VLM_MODEL
entries while parsing the file and immediately discards unrelated entries. It makes one strict
OpenAI-compatible decision with at most one format repair and no deterministic
fallback, then sends the decision and bounded Artifact bytes over the
authenticated loopback listener. Provider endpoints are learned only from
signed Directory Manifests. The physical acceptance runbook and redacted
evidence boundary are in
docs/testing/two-device-acceptance.md.
Required Walkman alias names are OPENAI_BASE_URL, OPENAI_API_KEY, and VLM_MODEL. For this command, OPENAI_BASE_URL is the complete ModelHub gemini_multimodal_inline_v1 endpoint: the adapter does not append a route, places the credential only in the ak query parameter, and sends inline text content. Only the Requester child receives these three variables. The other three children are started with a cleared environment. The demo never falls back to DeterministicDecisionAdapter; an invalid model response fails explicitly after one format-repair request.
The successful command prints one JSON summary containing:
- four distinct PIDs, HTTPS loopback addresses, Node IDs, and state directories;
- source and verification Contract IDs;
Proposed → Active → Running → Delivered → Accepted;- the immutable Artifact hash and both metric results;
- LLM call count, peer HTTP request/byte counters, and phase timings;
- the retained
.local/demo/<run-id>/directory for inspection.
Private keys and the local control token are stored with mode 0600. Runtime state, logs, private keys, tokens, and JSONL journals are ignored by Git. Peer payloads never accept a filesystem path.
demo harness (HTTPS/mTLS; loopback aliases when assigned)
├─ Directory loopback:dynamic
├─ Requester loopback:dynamic ── real LLM decision
├─ Executor loopback:dynamic ── source.metrics.v1
└─ Verifier loopback:dynamic ── source.metrics.verify.v1
Requester → Directory → signed CandidateSet
Requester → Executor → bilateral source Contract → Delivered Evidence
Requester → Directory → signed CandidateSet
Requester → Verifier → parent-linked verification Contract → Delivered Evidence
Requester → Executor → signed Accepted Event
agenet demo is provisioning and test scaffolding, not a control plane. Its four credentials are least privilege: Directory-only, Requester-only, Executor-only with the executor capability ceiling, and Verifier-only with the verifier ceiling. The Requester child receives only the Directory seed; Executor and Verifier endpoints are learned from signed Capability Manifests.
cargo fmt --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-targetsTests include pure protocol/property checks, storage replay, Axum oneshot authorization and limits, Reqwest timeout/error handling, LLM JSON repair and secret sentinels, metric edge cases, and a real four-child-process loopback demo backed by a clearly synthetic local completion server.
Plain HTTP remains restricted to loopback. A non-loopback peer endpoint must
use HTTPS with one explicit Authority CA, a required client certificate, an
exact IP SAN, and one canonical noncritical AgenNet NodeId extension. Inbound
requests bind that certificate NodeId to the signed envelope issuer; outbound
clients additionally require the expected remote NodeId from a verified seed
or signed Capability Manifest. They do not use system roots, ambient proxies,
or redirects. Revoked certificates fail at the TLS boundary; stale revocation
state preserves health diagnostics while effectful handlers fail closed.
Signed responses complete credential-chain, role, domain, expiry, and signature
verification before their verified issuer is compared with the TLS peer NodeId;
an invalid signed response is never classified as a TLS identity mismatch.
Client construction also requires its own leaf to contain exactly the declared
local boundary IP, preventing a valid peer identity issued for one overlay
address from being reused from another configured address.
Both directions require the SAN extension to contain exactly one entry: the
expected IP. A matching IP plus any additional IP or DNS identity is rejected.
Peer private-key PEM contains exactly one PKCS#8 block; mixed or trailing key
blocks fail configuration. Public /v0 routers also fail closed when neither
Rustls certificate metadata nor connection-layer loopback metadata is present,
so embedding a peer router in a plain non-loopback server cannot silently skip
the mTLS identity boundary.
The real TLS tests currently use dynamic loopback ports to exercise the same
Rustls/Reqwest handshake path. This is not evidence of physical multi-machine
reachability. Separate OS processes and state directories are not claimed as a
secure sandbox. The workload does not execute shell commands. A future
project.build_test.v1 adapter must use Docker, a VM, or a platform sandbox
before accepting untrusted code.
One typed CLI path is being added for private Tailscale and WireGuard Domains:
agenet domain init --network tailscale --bind-ip <assigned-ip>
agenet domain init --network wireguard --bind-ip <assigned-ip> \
--allowed-cidr <private-cidr>
agenet invite create --profile <base|provider|agent-candidate> --ttl 10m
agenet node join [--bind-ip <assigned-ip>]
Root passphrases and complete invitations use only the controlling terminal;
they are not accepted through argv, environment variables, JSON, or ordinary
stdin. node join returns credential_issued instead of claiming registration
or health. The current join result has no next command;
domain init advertises only the existing invite create command.
After enrollment, the lifecycle surface is:
agenet node start|stop|status
agenet credential renew
agenet node revoke <node-id>
agenet node leave
agenet uninstall [--purge]
agenet node doctor --output <text|json>
Renewal keeps the Node Ed25519 identity and rotates the TLS private key through a fresh CSR. Startup identity is stored as complete, owner-only UUID generations with exact file hashes. A single atomic active pointer selects one generation; once present, the runtime never combines new and legacy files. Renewal restarts the actual user service and requires its readiness plus new-identity mTLS health before retired-key cleanup. Cleanup uncertainty retains old material and emits a warning rather than claiming deletion.
Identity lifecycle mutations are serialized at two levels. Credential renewal
and destructive purge hold one owner-only identity-operation-v1.lock across
the entire workflow. Startup readers take a short shared advisory lock on the
owner-only identity-generation directory, while pointer publish/rollback,
inactive cleanup, and purge take it exclusively. The exclusive pointer lock is
released before restarting the user service, so HostRuntime can load the newly
selected generation without deadlocking. These locks coordinate honest AgenNet
processes under one account; a malicious same-euid process that ignores
advisory locks remains outside the v0.2 threat boundary.
node revoke is available only on the founding administrative host and requires
a controlling TTY, exact NodeId confirmation, and hidden Domain Root unlock.
node leave stops the service but retains identity, config, credentials,
journals, and audit state; Directory outage leaves a durable pending departure.
The pending record contains one exact signed request and operation ID reused by
later node leave invocations, including after local Left. A Directory-signed
receipt is durably validated and stored before pending state is cleared; missing
or invalid receipt state is never reported as a recorded departure.
Default uninstall retains all state and removes a binary only when its recorded
path, owner, mode, device, inode, size, hash, version, basename, and approved
per-user installation root still match. --purge is destructive, requires both
the exact NodeId and PURGE on the controlling TTY, and always retains Domain
Root plus founding Authority/administrative material.
Doctor is read-only and bounded. JSON output is deterministic and contains only
stable check code, ok|warn|error|skipped, sanitized message, remediation link,
and a stable overall exit code: 0 for all OK/skipped, 1 for warnings, 2 for any
error, and 3 only when report output itself fails. It does not refresh state.
Physical two-device reachability and setup automation remain Task 14 gates.