Local Agent Mesh Quickstart
This is a runnable introduction to the Local Agent Mesh: a vendor-neutral coordination layer that lets A2A Mesh route tasks to heterogeneous coding-agent workers (mock workers, generic local CLI tools, or future Claude/Codex/Copilot/opencode-style adapters), collect standardized artifacts, and keep every step auditable.
What this is, and is not
Is:
- A worker adapter contract (
WorkerRuntimeContractin@a2amesh/internal-worker-runtime) that any coding agent can implement without the core runtime knowing which vendor is behind it. - A policy-aware router (
routeFleetTaskin@a2amesh/internal-fleet) that matches tasks to workers by capability, workspace scope, risk level, and concurrency. - A generic local CLI adapter (
LocalCliWorkerRuntimeAdapter) that runs an allowlisted command in a scoped workspace with no secret passthrough by default. - A standardized artifact contract (
validateFleetArtifact) for plans, diffs, patches, logs, test output, review comments, security findings, PR metadata, and release evidence.
Is not:
- A way to bypass any provider's authentication, sandboxing, or terms of service.
- An unattended merge/publish/secret-access/destructive-terminal-execution system. Every
remote-write/publish/deployside effect requires an explicit approval gate (FleetApprovalGate,FleetPolicyDecision— see Policy, Sandbox, Artifact, and Approval Boundaries). - A session-scraping or credential-extraction tool for any agent provider (
unsafeSessionScrapingAllowedis alwaysfalseinMissionControlPlan).
Architecture at a glance
FleetTask
-> routeFleetTask() (capability + workspace + risk + concurrency match)
-> WorkerRuntimeContract.prepare() -> start() -> stream() -> verify() -> finalize() -> cleanup()
-> validateFleetArtifact() (schema, provenance, redaction)
-> audit trail / task storage- Registry: workers are represented as
FleetWorkerDiscoveryRecord(capabilities, roles, tenants, status). The minimal demo below uses a plain in-memory array (StaticWorkerDirectory); Registry-backed worker discovery replaces that array with live discovery against@a2amesh/registry. - Adapter: implements the
WorkerRuntimeContractlifecycle (packages/worker-runtime/src/types/lifecycle.ts). Two reference implementations ship today:MockWorkerRuntimeAdapter(no process, deterministic, good for tests/demos) andLocalCliWorkerRuntimeAdapter(spawns a real allowlisted local command). - Task router:
routeFleetTaskandplanFleetDispatchWavesinpackages/fleet/src/routing/TaskRouter.ts. - Policy: side-effect boundaries and approval gates are modeled by
FleetPolicyDecision,FleetSandboxProfile, andFleetApprovalGateinpackages/fleet/src/types/domain.ts. - Artifact exchange:
validateFleetArtifactinpackages/fleet/src/artifact-contracts/FleetArtifacts.ts. - Audit trail: pair this with the SQLite task storage audit journal (
docs/packages/runtime.md#audit-journal) when persisting task history.
Minimal demo: two mock workers routing a task
import { MockWorkerRuntimeAdapter } from '@a2amesh/internal-worker-runtime';
import { routeFleetTask, type FleetRoutingCandidate } from '@a2amesh/internal-fleet';
const reviewer = new MockWorkerRuntimeAdapter({
id: 'reviewer-1',
card: {
protocolVersion: '1.0',
name: 'Reviewer',
description: 'Reviews diffs for style and correctness',
url: 'local://reviewer-1',
version: '1.0.0',
},
steps: [{ message: 'reading diff' }, { message: 'posting review comment' }],
});
const candidates: FleetRoutingCandidate[] = [
{
worker: {
workerId: reviewer.id,
card: reviewer.card,
discoveredAt: new Date().toISOString(),
lastHeartbeatAt: new Date().toISOString(),
status: 'IDLE',
capabilities: ['code-review'],
roles: ['reviewer'],
},
activeRunCount: 0,
},
];
const decision = routeFleetTask(
{ taskId: 'task-1', requiredCapabilities: ['code-review'] },
candidates,
{ strategy: { type: 'CAPABILITY_MATCH' }, requiredSignals: ['capability', 'availability'] },
);
if (!decision.selectedWorkerId) {
throw new Error(`no safe agent available: ${decision.reason}`);
}
const context = {
task: { id: 'task-1', status: { state: 'WORKING', timestamp: new Date().toISOString() } },
worker: candidates[0].worker,
run: { id: 'run-1', taskId: 'task-1', workerId: reviewer.id, status: 'RUNNING' as const },
};
await reviewer.prepare(context);
await reviewer.start(context);
for await (const event of reviewer.stream(context)) {
console.log(event.type, event.message ?? '');
}
const result = await reviewer.finalize(context, {});
await reviewer.cleanup(context);
console.log('run finished with status', result.status);Run the equivalent test suite directly with:
pnpm --filter @a2amesh/internal-worker-runtime run test
pnpm --filter @a2amesh/internal-fleet run testWrapping a generic local CLI coding agent
LocalCliWorkerRuntimeAdapter wraps any local command-line tool as a worker. Nothing about it is Claude/Codex/Copilot/opencode-specific — it launches whatever command you configure, inside a scoped workspace, with no secret passthrough by default:
import { realpathSync } from 'node:fs';
import { LocalCliWorkerRuntimeAdapter } from '@a2amesh/internal-worker-runtime';
const cliExecutable = realpathSync('/opt/tools/my-coding-agent-cli');
const patchWorker = new LocalCliWorkerRuntimeAdapter({
id: 'patch-worker',
card: {
protocolVersion: '1.0',
name: 'Patch Worker',
description: 'Runs a local code-mod CLI',
url: 'local://patch-worker',
version: '1.0.0',
},
command: cliExecutable,
buildArgs: (context) => ['run', '--task', context.task.id],
artifactFiles: () => ['out/patch.diff', 'out/test-report.json'],
policy: {
commandAllowlist: [cliExecutable],
envAllowlist: [], // PATH and every other ambient value are denied by default
workspaceRoot: '/workspace/my-repo',
timeoutMs: 5 * 60_000,
maxConcurrentRuns: 2,
maxArtifactFiles: 8,
maxArtifactBytes: 5 * 1024 * 1024,
maxTotalArtifactBytes: 10 * 1024 * 1024,
allowedArtifactExtensions: ['.diff', '.json'],
},
});Security defaults, all enforced in code (see packages/worker-runtime/tests/LocalCliWorkerRuntimeAdapter.test.ts for the executable specification):
- Canonical executable allowlist:
commandand everypolicy.commandAllowlistentry must be an absolute canonical executable path. Bare names are rejected and the adapter never searches the hostPATH. - No secret passthrough by default: only environment variable names listed in
policy.envAllowlist, plus explicitenvvalues, are forwarded.PATHis not implicit; supply a controlled value explicitly only when the child process truly needs it. - Canonical workspace containment: the workspace root and working directory are resolved with
realpath; lexical escapes and symlink/junction traversal are rejected before spawning. - Output redaction: credential-shaped values and explicitly forwarded credential environment values are redacted from stdout/stderr events before they can be persisted or emitted.
- Timeouts and cancellation:
policy.timeoutMsaborts a hung run with a structuredTIMEOUTfailure (retryable: true);adapter.cancel(context, { reason })aborts an in-flight run on request and reports acanceledevent. - Concurrency limits:
policy.maxConcurrentRunsrejects new runs past the limit with aPOLICY_DENIEDfailure rather than queuing silently. - Fail-closed artifact capture: only declared, canonical, regular files inside the working directory are accepted. File identity is checked before and after a bounded descriptor read; symlinks/junctions, replacement races, devices, sockets, FIFOs, disallowed extensions, binary content (unless enabled), and size/count limit violations fail the run with
ARTIFACT_UNAVAILABLE. Missing declared files remain optional and are omitted. - Structured failures everywhere: policy denials, spawn errors, non-zero exits, timeouts, cancellations, and unsafe artifacts surface as a
WorkerRuntimeResultwith a populatedfailurefield — callers never need to catch an adapter-thrown exception to detect a failed run.
Platform behavior:
- Linux/macOS: artifact descriptors use
O_NOFOLLOW, inode/device identity checks, canonical path checks, and bounded reads. - Windows: canonical path and reparse-point/junction rejection plus pre/post file identity checks provide the fail-closed boundary available through Node.js. Use canonical drive-qualified executable and workspace paths.
Policy-backed official CLI admission
Use OfficialCliWorkerRuntimeAdapter from @a2amesh/internal-worker-cli above the generic runtime when a documented vendor CLI is the integration surface. The wrapper requires a FleetProviderWorkerPlan, binds the absolute executable to each FleetWorkerRunAdmission, forwards credential references by name only, and requires an audited APPROVED gate before local-write worktree mutation. remote-write, publish, and deploy remain denied; those actions require a separate operator-controlled integration.
The examples/local-cli-fleet smoke path demonstrates routing, explicit local approval, confined execution, and checksummed artifact verification without installing an external CLI.
Policy-backed MCP tool admission
Use McpWorkerRuntimeAdapter from @a2amesh/internal-worker-mcp when a documented MCP tool is the integration surface. The caller supplies and authenticates the MCP client; the worker invokes one configured tool from an explicit allowlist, validates task/worker/tool/side-effect admission, enforces timeout and concurrency limits, and converts bounded text output into a SHA-256 checksummed artifact. local-write requires audited maintainer approval, while remote-write, publish, and deploy fail closed.
The examples/mcp-bridge smoke path keeps the existing A2A/MCP mapping proof and adds a network-free Fleet worker lifecycle using an injected fake client.
A realistic workflow: issue triage to PR-ready patch
- Route:
routeFleetTaskpicks a worker capable ofpatch-generationfor the target workspace. - Plan: the worker's first artifact is a
plan(see artifact kinds below) describing the intended change. - Implement: the worker emits
task-updateprogress events while it edits files, then returns adifforpatchartifact plus afile-change-summary. - Test: a second worker (or the same one) runs the test suite and returns
test-outputand acommand-log. - Review: a reviewer worker (or a human) returns
review-commentartifacts. - Prepare the PR:
pr-metadataandrelease-evidenceartifacts are assembled for a human to open the actual pull request — the mesh does not open, merge, or push PRs on its own by default; that is aremote-write/publishside effect gated byFleetApprovalGate.
planFleetDispatchWaves can express step 3/4's dependency (tests depend on the patch landing) as a two-wave dispatch plan; unrelated tasks (e.g., two independent review comments) land in the same wave and can run in parallel.
Registry-backed worker discovery
The minimal demo above builds the candidate array by hand. routeFleetTask/planFleetDispatchWaves never assumed that array was static — FleetWorkerDirectory (packages/fleet/src/discovery/) makes the candidate source an explicit, swappable seam:
StaticWorkerDirectorywraps a fixed array — the in-memory-list behavior above, made explicit.RegistryWorkerDirectorypolls a live@a2amesh/registryinstance instead: it querieslistAgents()on a bounded refresh interval, evicts unhealthy or stale-heartbeat agents before they ever reach the router, and falls back to the last known-good candidate set (or a configured static fallback) if the registry is temporarily unreachable, so a registry outage degrades routing rather than breaking it.
import { AgentRegistryClient } from '@a2amesh/runtime';
import { RegistryWorkerDirectory, routeFleetTask } from '@a2amesh/internal-fleet';
// AgentRegistryClient#listAgents() already returns the fields RegistryWorkerDirectory
// needs (id, card, status, skills, tenantId, lastHeartbeatAt) — no adapter code required.
const registry = new AgentRegistryClient('http://127.0.0.1:3099');
const directory = new RegistryWorkerDirectory(registry, {
refreshIntervalMs: 5_000, // default; how often listAgents() is re-queried
staleAfterMs: 60_000, // default; agents with an older heartbeat are evicted
activeRunCounts: () => currentRunCountsByWorkerId(), // your own load tracking
});
const candidates = await directory.listCandidates();
const decision = routeFleetTask(
{ taskId: 'task-1', requiredCapabilities: ['code-review'] },
candidates,
{ strategy: { type: 'CAPABILITY_MATCH' }, requiredSignals: ['capability', 'availability'] },
);Workers publish themselves to the registry the same way any A2A agent does — call registry.register(agentUrl, workerCard, { tenantId }) on startup with a WorkerCard (fleetRoles, maxConcurrentTasks) as the agentCard; no separate fleet-specific registration API is needed. RegistryWorkerDirectory never dispatches to a worker the registry reports as unhealthy, and tenantScoped routing policies are enforced the same way as with a static list, since a registry-backed candidate carries the same FleetWorkerDiscoveryRecord shape.
Artifact exchange
Every artifact a worker returns is validated with validateFleetArtifact before being trusted:
import { validateFleetArtifact } from '@a2amesh/internal-fleet';
const artifact = validateFleetArtifact({
artifactId: 'run-1:patch',
kind: 'patch',
taskId: 'task-1',
contentType: 'text/x-diff',
sensitivity: 'internal',
redacted: false,
provenance: { producerId: 'patch-worker', taskId: 'task-1', runId: 'run-1', branch: 'main' },
createdAt: new Date().toISOString(),
content: 'diff --git a/src/index.ts b/src/index.ts\n...',
});Standardized kinds: plan, diff, patch, file-change-summary, command-log, test-output, review-comment, security-finding, pr-metadata, release-evidence. Validation rejects unknown kinds, mismatched provenance, oversized inline payloads (200 KB — use payloadRef for larger artifacts), unapproved payloadRef schemes or embedded credentials, and any artifact whose content looks credential-shaped (API keys, bearer tokens, private key headers) unless it is marked redacted: true. sensitivity: 'restricted' artifacts must always be redacted before they validate.
Security defaults (summary)
| Default | Where it is enforced |
|---|---|
| No secrets passed to worker processes | LocalCliWorkerRuntimeAdapter env allowlist and output redaction |
| Canonical executable allowlist | resolveWorkerExecution(); ambient PATH lookup disabled |
| Workspace and artifact confinement | realpath checks plus descriptor/inode validation |
| Destructive/remote actions gated | FleetApprovalGate, FleetPolicyDecision (see policy doc) |
| Artifacts scanned for credentials | validateFleetArtifact |
| No session scraping | MissionControlPlan.unsafeSessionScrapingAllowed fixed to false |
| Fail-closed routing | routeFleetTask returns no selectedWorkerId rather than guessing |
Troubleshooting
- "command ... must be an absolute executable path": resolve the binary once at configuration time (for example
realpathSync(process.execPath)) and use that same canonical value forcommandandpolicy.commandAllowlist. Bare names and PATH lookup are intentionally disabled. - "working directory ... escapes workspace root" / "resolves through a symbolic link":
cwdtried to leave the canonical workspace or traverse a symlink/junction. Use a real directory insidepolicy.workspaceRoot; do not widen the root merely to accommodate an untrusted link. - Run never completes / times out: increase
policy.timeoutMs, or check whether the wrapped CLI is waiting on stdin — the adapter runs withstdio: ['ignore', 'pipe', 'pipe'], so an agent that blocks on interactive input will hang until the timeout fires. - Environment variable the CLI needs is missing: add its name to
policy.envAllowlist(or pass an explicit value viaenv) — nothing is forwarded implicitly. - Declared artifact file is missing from the result: a file that was never produced is optional and omitted. An existing path that escapes confinement, traverses a link/junction, is not a regular file, changes during capture, violates extension/content policy, or exceeds a limit fails closed with
ARTIFACT_UNAVAILABLE; inspect the final failure event. routeFleetTaskreturns noselectedWorkerId: readdecision.reason— it names exactly which filter emptied the candidate set (capability, workspace scope, concurrency, tenant, or missing approval for a risk level).