@auggy/link
v0.1.2
Published
Link — the v1 entry point to the Mesh. Peer-to-peer agent ↔ agent over A2A v0.2 (JSON-RPC), mutual bearer auth, durable task store, no central service. See ADR-022.
Downloads
536
Readme
@auggy/link
Peer-to-peer agent ↔ agent over A2A v0.2. Reference implementation in
TypeScript on Bun. Receiver-side Hono app + outbound PeerClient + durable
SQLite store + bearer auth + streaming + recovery contracts. Library, not
service — consumers wire their own Bun.serve and supply a MessageHandler.
Mesh is the destination. Link is how aug1s (and any A2A-speaking runtime) join the mesh today. See ADR-022 for the destination-vs-entry framing and the implementation plan for the v0.1 anchor use cases. (PLAN.md is not shipped in the npm tarball — implementation history lives in the source repo.)
Status: v0.1.0
What's locked at v0.1:
- A2A v0.2 protocol — JSON-RPC envelope, types, schemas, agent-card discovery
message/send,message/stream,tasks/get,tasks/cancel- Receiver-side Hono app via
createLinkApp BearerAuthProviderwith rotation overlap windowsSqliteTaskStore(durable, WAL mode) +MemoryTaskStore(test only)PeerClientoutbound — retry, exponential backoff, Message-vs-Task return shape discrimination,tasks/get-based stream reattachEnvAddressBook— peer config from env vars- Per-peer + global anonymous rate limiting
- TLS termination + proxy header trust gating (
TlsTrustConfig) - Idempotency-Key dedup with TTL eviction; restart-survival of replays
- Stall sweep for in-flight tasks
- SIGTERM drain —
installSigtermHandleris the canonical wire-up - Railway deployment template (in the source repo:
infra/Dockerfile,infra/deploy.md— the npm tarball is library-only)
What's deferred to v0.2+:
tasks/resubscribe(SSE re-attach without re-issuing the task)- Push notifications method group
- File / image / non-text
Partkinds (text-only at v0.1) - Multi-replica deployment with shared Postgres (single-instance per service at v0.1; PLAN §6.4 forbids multi-instance shared-DB)
- Hot-reloadable address book (snapshot at construction at v0.1)
- Auto-rotation of bearers (manual rotation runbook at v0.1)
- Channels, federation, cross-org trust (separate sequencing per ADR-022)
- Node compat (Bun-only at v0.1; the SQLite store imports
bun:sqlite)
Install
bun add @auggy/linkRequires bun >= 1.0.0. Node is not supported at v0.1.
Quickstart
A receiver wired to Bun.serve, with bearer auth and an in-memory task store
(swap MemoryTaskStore for SqliteTaskStore in production):
import {
buildAgentCard,
BearerAuthProvider,
createLinkApp,
type HandlerOutcome,
type MessageHandler,
} from "@auggy/link";
import { MemoryTaskStore } from "@auggy/link/testing";
const agentCard = buildAgentCard({
id: "00000000-0000-4000-8000-000000000001",
name: "Demo Receiver",
description: "Echoes incoming messages",
endpoint_url: "https://demo.example.com",
capabilities: [],
skills: [],
});
const auth = new BearerAuthProvider({
peers: {
"00000000-0000-4000-8000-000000000002": {
participant: {
id: "00000000-0000-4000-8000-000000000002",
locator: "https://peer-b.example.com",
type: "agent",
trust: "agent",
},
active: {
bearer: process.env.PEER_B_INBOUND_BEARER!,
bearer_id: "v1",
},
},
},
});
const onMessage: MessageHandler = async ({ message }): Promise<HandlerOutcome> => {
return {
kind: "message",
parts: [{ kind: "text", text: "echo: " + JSON.stringify(message.parts) }],
};
};
const app = createLinkApp({
agentCard,
auth,
taskStore: new MemoryTaskStore(),
onMessage,
});
Bun.serve({ port: 8080, fetch: app.fetch });Use SqliteTaskStore in production — it survives restart and satisfies
Anchor III (originator restart, return-to-completed task):
import { SqliteTaskStore } from "@auggy/link";
const taskStore = new SqliteTaskStore({ path: "/data/link.db" });For graceful shutdown, install the SIGTERM handler against the
LinkAppHandle:
import { installSigtermHandler } from "@auggy/link";
const server = Bun.serve({ port: 8080, fetch: app.fetch });
installSigtermHandler({
fetch: app.fetch,
shutdown: async () => {
await app.shutdown();
server.stop(true);
},
});Public API surface
Every export below is locked at v0.1. Internal modules can be reorganized without a major version bump; this list cannot.
Protocol — import { ... } from "@auggy/link"
| Export | Kind | Purpose |
|---|---|---|
| buildAgentCard | function | Build a validated AgentCard for /.well-known/agent.json |
| BuildAgentCardOptions | type | Options for buildAgentCard |
| parseRequest | function | Validate + decode a JSON-RPC request envelope |
| successResponse | function | Build a JSON-RPC success envelope |
| errorResponse | function | Build a JSON-RPC error envelope |
| parseError, invalidRequest, methodNotFound, invalidParams, internalError, taskNotFound, taskNotCancelable, pushNotificationNotSupported, unsupportedOperation, contentTypeNotSupported | factories | A2A error factory functions |
| PARSE_ERROR, INVALID_REQUEST, METHOD_NOT_FOUND, INVALID_PARAMS, INTERNAL_ERROR, TASK_NOT_FOUND, TASK_NOT_CANCELABLE, PUSH_NOTIFICATION_NOT_SUPPORTED, UNSUPPORTED_OPERATION, CONTENT_TYPE_NOT_SUPPORTED | constants | Numeric A2A error codes |
| MessageSendParamsSchema, MessageSendWireResultSchema, MessageStreamParamsSchema, TasksGetParamsSchema, TasksGetWireResultSchema, TasksCancelParamsSchema, TasksCancelWireResultSchema, HandlerSendResultSchema | zod schemas | Per-method runtime validators |
| MessageSendParams, MessageSendWireResult, MessageStreamParams, TasksGetParams, TasksGetWireResult, TasksCancelParams, TasksCancelWireResult, HandlerSendResult | types | Per-method type aliases |
| hashSendPayload, isTaskResult | functions | Idempotency-key payload hash + result-shape predicate |
| Result | type | { ok: true; value } | { ok: false; error } |
| AgentCard, Artifact, ISO8601, JsonRpcError, JsonRpcErrorPayload, JsonRpcRequest, JsonRpcResponse, Message, MessageRole, Part, Participant, ParticipantType, SkillDescriptor, SkillReturns, Task, TaskState, TextPart, TrustLevel, UUID | types | Wire-shape types |
| AgentCardSchema, ArtifactSchema, JsonRpcErrorPayloadSchema, JsonRpcErrorSchema, JsonRpcRequestSchema, JsonRpcResponseSchema, MessageRoleSchema, MessageSchema, ParticipantSchema, ParticipantTypeSchema, PartSchema, SkillDescriptorSchema, SkillReturnsSchema, TaskSchema, TaskStateSchema, TextPartSchema, TrustLevelSchema | zod schemas | Wire-shape validators |
Server — import { ... } from "@auggy/link"
| Export | Kind | Purpose |
|---|---|---|
| AuthProvider | interface | Pluggable receiver-side auth contract |
| AuthContext, AuthFailureCode, AuthVerifyRequest | types | Auth provider input/output shapes |
| AuthFailure | class | Auth verify failure |
| LinkAuthError | class | Auth error class |
| BEARER_EXPIRED_HEADER, BEARER_EXPIRED_VALUE | constants | Bearer-expired wire signal |
| BearerAuthProvider | class | Bearer auth implementation with rotation slots |
| BearerAuthProviderOptions, PeerBearerConfig | types | Options for BearerAuthProvider |
| MessageHandler | type | Wrapper-supplied handler callback |
| HandlerContext, HandlerOutcome, MessageOutcome, TaskCreateOutcome, TaskContinueOutcome, TaskContinueOutcomeWorking, TaskContinueOutcomeInputRequired, TaskContinueOutcomeCompleted, TaskContinueOutcomeFailed, TaskContinueOutcomeCanceled, ErrorOutcome | types | Handler input + outcome variants |
| TaskStore | interface | Persistence contract for receiver-side task state |
| CreateTaskInput, LeaseToken, LinkStoreErrorCode, TaskStateTransition | types | Task store input shapes |
| LEGAL_TRANSITIONS, isLegalTransition | constant + function | Task state-machine guard |
| LinkStoreError, StoreError, TaskNotFound, TaskTerminal, IdempotencyConflict, LeaseExpired, StaleStateTransition | classes | Task-store error hierarchy |
| SqliteTaskStore | class | Durable production TaskStore (WAL-mode SQLite) |
| SqliteTaskStoreOptions | type | Constructor options for SqliteTaskStore |
Public API factories + classes — import { ... } from "@auggy/link"
| Export | Kind | Purpose |
|---|---|---|
| createLinkApp | function | Receiver-side factory; returns LinkAppHandle |
| LinkAppOptions, LinkAppHandle, Fetch | types | Factory shapes |
| RateLimitsConfig, TlsTrustConfig | types | Optional rate-limit + TLS-trust policy |
| AddressBook | interface | Outbound peer config lookup |
| EnvAddressBook | class | Env-var-backed AddressBook |
| PeerClient | class | Outbound A2A client |
| PeerClientConfig, TaskStreamEvent | types | Outbound config + stream event shape |
| DEFAULT_PEER_CLIENT_CONFIG | constant | Default retry + timeout settings |
| PeerSendError, PeerClientErrorCode | types | Outbound error shapes |
| LinkClientError, PeerUnauthenticated, PeerNetworkError, PeerProtocolError, PeerRateLimited, PeerConfigError | classes | Outbound error hierarchy |
Runtime — import { ... } from "@auggy/link"
| Export | Kind | Purpose |
|---|---|---|
| installSigtermHandler | function | Wire SIGTERM/SIGINT to LinkAppHandle.shutdown |
| InstallSigtermHandlerOptions | type | Options for installSigtermHandler |
Test-only — import { ... } from "@auggy/link/testing"
| Export | Kind | Purpose |
|---|---|---|
| MemoryTaskStore | class | In-process TaskStore for tests; lost on restart |
| createDevApp | function | Bare Hono app exposing /health for bun run dev liveness |
The ./testing subpath is deliberately separate so production consumers
cannot accidentally pull MemoryTaskStore and lose state on every restart
(which would silently break Anchor III re-attachment).
A2A v0.2 wire compatibility
Link v0.1 targets A2A v0.2 only (Google + Linux Foundation, JSON-RPC over
HTTPS, agent-card discovery at /.well-known/agent.json).
In scope at v0.1:
- Methods:
message/send,message/stream,tasks/get,tasks/cancel - Parts:
textonly - Bearer auth (the v0.2 spec admits mTLS and other schemes; we ship bearer)
- Agent card discovery
- A2A-spec error codes — every receiver-side error maps to a documented code
Deferred (still A2A-compatible, just not implemented in v0.1):
tasks/resubscribe— SSE re-attach without re-issuing the task- Push notifications method group
- File / image parts
- mTLS auth scheme
Mismatched-version peers (a future link-v0.2 talking to a link-v0.1) are an
operational concern at the deployment layer, not at the library level. The
AgentCard.capabilities field is the discovery point; future link versions
will read it and decline unsupported method groups gracefully.
Two ways to use link
As a library (npm)
bun add @auggy/linkThis is what the npm tarball delivers: compiled library code (dist/),
type declarations, README, and LICENSE — nothing else. Wire it into your
own runtime using the Quickstart above:
createLinkApp + BearerAuthProvider + a TaskStore + your
MessageHandler + Bun.serve.
As a reference deployment (clone the repo)
The package ships compiled library code only. To deploy a real link
instance to Railway (or Docker, or any container runtime), clone the
repository — it has the Dockerfile, railway.json, and operator runbook:
git clone https://github.com/looselyorganized/link.git
cd link
# follow infra/deploy.mdThe Railway template, Dockerfile, and full deployment guide live at:
These deliberately are NOT shipped in the npm tarball — link is a library,
not an operator distribution. v0.2 may publish a separate scaffolding
package (a la create-next-app).
For repo contributors verifying the integration test suite from a clone:
bun test tests/integration/two-instance-roundtrip.test.ts spawns two
Bun.serve instances on different ports and exercises message/send +
tasks/get round-trips with mutual bearer auth. (tests/ is dev-only and
excluded from files, so this command only works against a clone, not
against the tarball.)
Semver + breaking-change policy
v0.x is pre-1.0. Minor versions MAY break; patch versions never break. v1.0+ follows strict semver: major = breaking, minor = additive, patch = bugfix.
What counts as breaking
- Removing a public export from
@auggy/linkor@auggy/link/testing - Narrowing an exported type's domain (e.g., adding a required option field, removing a union variant from an output type)
- Renaming a public export (treated as remove + add)
- Removing or renaming a method on an exported class
- Removing a method from an exported interface (consumer-side impls would silently break — see caveat below)
- Changing the wire protocol in a way that breaks A2A v0.2 compatibility (the wire is locked to A2A; link-level breaks only ship in a major bump)
- Tightening a runtime validator's constraints (formerly-valid input rejected)
What does NOT count as breaking
- Adding optional fields to options types
- Adding new exports
- Adding new error subclasses to an existing hierarchy (consumer
instanceofchecks still work; new ones can be added in user code) - Loosening a validator's constraints
- Refactoring internal modules — the public barrel is the contract
Interface-method caveat
Adding a method to an exported interface is technically additive for callers
but breaking for implementors (consumers who provide their own
AuthProvider, TaskStore, AddressBook, MessageHandler). Link treats
this as a minor at v1.0+ but ships a deprecation cycle: a default impl
is provided on the interface for one minor version, and removed only at the
next major. v0.1 makes no such promise — we may add interface methods
freely until v1.0.
A2A version support
Link v0.x targets A2A v0.2 only. Future link versions will track A2A spec
evolution. Mismatched-version peers (e.g., a link-v1.0 talking to a link-v2
peer that has dropped a method group) are an operational concern handled
through AgentCard.capabilities discovery at the deployment layer, not at
the library API level.
License
See LICENSE. License decision is deferred at v0.1.0 pending review by the LORF maintainers; the code is currently published under "all rights reserved" — clone for evaluation, but contact the maintainers before production use. A v0.2 release will commit to a specific OSI-approved license.
Contributing
Link is part of the Loosely Organized Research Facility (LORF) project. PRs are welcome, but the public API surface listed above is locked — additions follow the additive rules in the semver policy; removals and renames need a major version bump. File-level reorganization is fine, but the barrel exports cannot move.
For inquiries: [email protected]
