Agent-net, the team building an agent-to-agent marketplace where AI agents discover, trust, and pay each other, has released Webagent, an open source harness for standing up public-facing business agents. So, basically you give it your website, get an agent, and let it talk to other agents. Instead of writing orchestration code, a business fills in a declarative JSON spec, picks 1 provider for each of 9 pluggable slots, and runs webagent serve.
Is it deployable? Yes, with caveats. The repo ships under Apache 2.0, builds green, and runs live Slack, WhatsApp, and HTTP agents backed by MCP tools today. It is still labeled v0, and the browser action provider, OAuth-gated MCP, OTel export, and the AgentNet identity and billing layer are listed as not yet built.
Slots, providers, picks
Webagent is written in Go. An agent is 1 Brain (an LLM plus instruction) over a set of slots defined in core/. Each slot is a Service Provider Interface with a registry of providers in spi/ and a designated default:
| Slot | Built-in providers | Default |
|---|---|---|
| Retrieval | live, keyword, hybrid | live |
| Memory | session | session |
| Guardrail | basic, off | basic |
| Channel | a2a, web, slack, whatsapp (telegram stub) | a2a |
| Secrets | env, file, static | env |
| Presenter | text, terminal (QR), web | text |
| Model | echo, openrouter, gateway | echo |
| Action | none, demo, mcp | none |
| Observability | none, log, memory | none |
The design serves 3 audiences on 1 contract: businesses that configure by picking from the menu, businesses that extend by registering a custom provider, and partner companies that ship adapters. Nobody forks the core. Every provider must pass its slot’s conformance suite to be certified.
Safety is code-enforced, not prompt-enforced
The most important architectural decision is action.Guard. Every tool the agent holds, whether it comes from the action provider or is injected by the host, is wrapped so the chosen guardrail runs on the action before it executes. The model cannot bypass it. The DESIGN.md frames the whole project around a research finding that architecture, not model capability, decides agent success, citing arXiv 2511.19477 and an 85% versus 50% task-success gap on the same models.
What runs today
The default echo brain needs no credentials, so webagent validate and webagent serve work out of the box on the 2 example specs, zomato.json and bakery.json. To drive a real model, webagent keys set openrouter stores a key in the OS config directory with mode 0600. Keys are never written into a spec, and an exported environment variable always wins. Both openrouter and gateway are OpenAI-compatible clients with a tool-calling loop.
A business with an existing MCP server becomes an acting agent with 1 spec block. The mcp action provider connects over Streamable HTTP (JSON and SSE) with bearer or API-key auth, performs the handshake at build time, and hands each tool to the agent behind the guard, so validate reports the real tool count.
The Slack and WhatsApp channel adapters verify every inbound webhook signature, acknowledge immediately, reply through the platform API, ignore their own messages, and de-duplicate retried deliveries. Slack points at /slack/events; Meta’s callback goes to /whatsapp/webhook.
Secrets follow a naming rule: any config key ending in Secret is a reference resolved through the selected vault at build time, so specs are safe to commit. A reference that cannot be resolved fails the build rather than starting a channel without a credential. Observability emits a per-turn TurnTrace aligned to the OpenTelemetry GenAI conventions, and an eval/ harness runs scenarios with checks.
function esc(s){ return String(s).replace(/&/g,’&’).replace(/”‘+k+'”: “‘+esc(v)+'”‘; }
function render(){
var lines = [‘{‘];
lines.push(kv(‘name’,’my-business-agent’)+’,’);
lines.push(‘ “model”: { “type”: “‘+pick.model+'”‘+(pick.model!==’echo’?’, “config”: { “model”: “…” }’:”)+’ },’);
lines.push(‘ “action”: { “provider”: “‘+pick.action+'”‘+(pick.action===’mcp’?’, “mcpUrl”: “https://your-server/mcp”‘:”)+’ },’);
lines.push(kv(‘retrieval’,pick.retrieval)+’,’);
lines.push(kv(‘memory’,pick.memory)+’,’);
lines.push(kv(‘guardrail’,pick.guardrail)+’,’);
lines.push(‘ “channels”: [ { “type”: “‘+pick.channel+'”, “presenter”: “‘+pick.presenter+'” } ],’);
lines.push(kv(‘secrets’,pick.secrets)+’,’);
lines.push(kv(‘observability’,pick.observability));
lines.push(‘}’);
lines.push(‘// illustrative shape; see examples/*.json in the repo for exact keys‘);
root.querySelector(‘#spec’).innerHTML = lines.join(‘\n’);
var v = root.querySelector(‘#val’);
var notes = [];
if (pick.channel===’telegram’) notes.push(‘telegram is a stub in the current repo, so validate would not resolve a live adapter.’);
if (pick.model!==’echo’) notes.push(‘needs an API key: run webagent keys set ‘+pick.model+’ (never written into the spec).’);
if (pick.action===’mcp’) notes.push(‘validate performs the MCP handshake and reports the real tool count.’);
if (pick.guardrail===’off’) notes.push(‘guardrail off: action.Guard still wraps every tool, but the check passes everything.’);
if ((pick.channel===’slack’||pick.channel===’whatsapp’)) notes.push(pick.channel+’ needs *Secret references (for example botTokenSecret) that resolve through the ‘+pick.secrets+’ vault, or the build fails.’);
v.classList.toggle(‘err’, pick.channel===’telegram’);
v.innerHTML = (pick.channel===’telegram’ ? ‘webagent validate: ‘ : ‘webagent validate: 9/9 slots resolved. ‘) + (notes.length ? notes.join(‘ ‘) : ‘Default picks run with no credentials at all.’);
sendH();
}
// trace
var pipeEl = root.querySelector(‘#pipe’), logEl = root.querySelector(‘#log’);
function nodes(){
return [
{id:’ch’, t:’Channel’, s:pick.channel},
{id:’br’, t:’Brain’, s:pick.model},
{id:’gd’, t:’action.Guard’, s:’wraps tool’},
{id:’gr’, t:’Guardrail’, s:pick.guardrail},
{id:’tl’, t:’Tool’, s:pick.action},
{id:’ob’, t:’Observer’, s:pick.observability},
{id:’rp’, t:’Reply’, s:pick.presenter}
];
}
function drawPipe(){
pipeEl.innerHTML = ”;
nodes().forEach(function(n){
var d = document.createElement(‘div’); d.className=”node”; d.id=’n-‘+n.id;
d.innerHTML = n.t + ‘‘+esc(n.s)+’‘;
pipeEl.appendChild(d);
});
sendH();
}
var timers = [];
function clearT(){ timers.forEach(clearTimeout); timers=[]; }
function step(ms, fn){ timers.push(setTimeout(fn, ms)); }
function lit(id, cls){ var e = root.querySelector(‘#n-‘+id); if(e){ e.className=”node “+(cls||’lit’); } }
function say(html, cls){ var d=document.createElement(‘div’); d.className=cls||”; d.innerHTML=html; logEl.appendChild(d); sendH(); }
function runTurn(risky){
clearT(); drawPipe(); logEl.innerHTML=”;
var btns = [root.querySelector(‘#run’), root.querySelector(‘#risky’)];
btns.forEach(function(b){ b.disabled=true; });
var t=0;
step(t+=100, function(){ lit(‘ch’); say(‘[channel:’+pick.channel+’] inbound message received’+(pick.channel===’slack’||pick.channel===’whatsapp’?’, webhook signature verified, ack sent’:”)+’.’); });
step(t+=700, function(){ lit(‘br’); say(‘[brain:’+pick.model+’] ‘+(pick.model===’echo’?’echo brain returns the input (no model call).’:’model reasons and ‘+(pick.action===’none’?’answers directly.’:’requests a tool call.’))); });
if (pick.action===’none’ || pick.model===’echo’) {
step(t+=700, function(){ lit(‘gd’,’skip’); lit(‘gr’,’skip’); lit(‘tl’,’skip’); say(‘no tools attached, guard path skipped.‘); });
} else {
step(t+=700, function(){ lit(‘gd’); say(‘[action.Guard] intercepts the call before execution.’); });
step(t+=700, function(){
if (risky && pick.guardrail===’basic’) { lit(‘gr’,’block’); say(‘[guardrail:basic] action blocked. Tool never runs.‘); }
else { lit(‘gr’); say(‘[guardrail:’+pick.guardrail+’] action allowed’+(pick.guardrail===’off’?’ (guardrail is off)’:”)+’.‘); }
});
step(t+=700, function(){
if (risky && pick.guardrail===’basic’) { lit(‘tl’,’skip’); }
else { lit(‘tl’); say(‘[tool:’+pick.action+’] ‘+(pick.action===’mcp’?’MCP tool executes over Streamable HTTP.’:’demo tool executes offline.’)); }
});
}
step(t+=700, function(){ lit(‘ob’); say(‘[observer:’+pick.observability+’] ‘+(pick.observability===’none’?’no TurnTrace recorded.’:’TurnTrace recorded (OTel GenAI-aligned fields).’)); });
step(t+=700, function(){ lit(‘rp’); say(‘[presenter:’+pick.presenter+’] reply delivered through ‘+pick.channel+’.‘); btns.forEach(function(b){ b.disabled=false; }); });
}
root.querySelector(‘#run’).addEventListener(‘click’, function(){ runTurn(false); });
root.querySelector(‘#risky’).addEventListener(‘click’, function(){ runTurn(true); });
// height messaging for iframe host
function sendH(){
try { var h = root.offsetHeight + 40; if (window.parent && window.parent !== window) window.parent.postMessage({mtpWebagentHeight:h}, ‘*’); } catch(e){}
}
render();
window.addEventListener(‘load’, sendH);
window.addEventListener(‘resize’, sendH);
})();
“>
Key Takeaways
- 1 JSON spec, 9 slots, 1 provider each;
webagent servedoes the rest - Every tool call passes through
action.Guardbefore execution - Live today: OpenRouter or gateway brains, MCP tools, Slack, WhatsApp, HTTP channels
- Not yet: browser actions, OAuth MCP, OTel export, AgentNet billing
- Apache 2.0, Go, v0; read the deferred-hardening list before production
Check out the GitHub repo and the launch post. All credit goes to the researcher of this project. Also, feel free to follow us on Twitter and don’t forget to join our 150k+ML SubReddit and Subscribe to our Newsletter. Wait! are you on telegram? now you can join us on telegram as well.
Need to partner with us for promoting your GitHub Repo OR Hugging Face Page OR Product Release OR Webinar etc.? Connect with us

