← back to AbramsEgo
TK-10381: A2A client Phases A-C (Steve-approved) — client-only, activation gated (empty allowlist)
048ab9b36582b2b45606f361bf195e170b65532c · 2026-08-09 09:15:54 -0700 · Steve
Phase A: lib/a2a-client.js hardened — HTTPS/443-only, 15s timeout, redirect:error,
card-driven SSRF guard (assertSameHost), 1MB response cap (OOM guard), URL-userinfo
rejection, payload linter (secrets can't leave), consult-log truncation. Tests pass.
Phase B: /api/a2a/consult + /api/a2a/agents routes (concurrency-capped, auth_ref env:
indirection), empty allowlist, append-only consult log, dashboard UNTRUSTED panel
(external answers rendered escaped + UNTRUSTED-EXTERNAL tagged, never fed to chat/agent).
Phase C: memos/a2a-egress-rails.md (egress-sentinel cross-ref) + live smoke tests.
Also fixed pre-existing officers innerHTML XSS (esc). Cody-gated (FIX FIRST -> all
findings fixed + re-verified). Activation still gated: empty allowlist refuses every
consult; Steve names a peer to turn it on.
Files touched
M data/local-fleet.jsonM lib/a2a-client.jsM lib/a2a-client.test.jsA memos/a2a-egress-rails.mdM public/index.htmlM server.js
Diff
commit 048ab9b36582b2b45606f361bf195e170b65532c
Author: Steve <steve@designerwallcoverings.com>
Date: Sun Aug 9 09:15:54 2026 -0700
TK-10381: A2A client Phases A-C (Steve-approved) — client-only, activation gated (empty allowlist)
Phase A: lib/a2a-client.js hardened — HTTPS/443-only, 15s timeout, redirect:error,
card-driven SSRF guard (assertSameHost), 1MB response cap (OOM guard), URL-userinfo
rejection, payload linter (secrets can't leave), consult-log truncation. Tests pass.
Phase B: /api/a2a/consult + /api/a2a/agents routes (concurrency-capped, auth_ref env:
indirection), empty allowlist, append-only consult log, dashboard UNTRUSTED panel
(external answers rendered escaped + UNTRUSTED-EXTERNAL tagged, never fed to chat/agent).
Phase C: memos/a2a-egress-rails.md (egress-sentinel cross-ref) + live smoke tests.
Also fixed pre-existing officers innerHTML XSS (esc). Cody-gated (FIX FIRST -> all
findings fixed + re-verified). Activation still gated: empty allowlist refuses every
consult; Steve names a peer to turn it on.
---
data/local-fleet.json | 2 +-
lib/a2a-client.js | 47 +++++++++++++++++++++++++++++++++++++++++----
lib/a2a-client.test.js | 23 ++++++++++++++++++++--
memos/a2a-egress-rails.md | 47 +++++++++++++++++++++++++++++++++++++++++++++
public/index.html | 49 ++++++++++++++++++++++++++++++++++++++++++++++-
server.js | 17 +++++++++++++++-
6 files changed, 176 insertions(+), 9 deletions(-)
diff --git a/data/local-fleet.json b/data/local-fleet.json
index 397fd044..5af29e93 100644
--- a/data/local-fleet.json
+++ b/data/local-fleet.json
@@ -1 +1 @@
-{"fetchedAt":"2026-08-09T15:45:05.270Z","procs":[{"name":"pm2-logrotate","status":"online","cpu":0.5,"memMB":85,"restarts":0,"uptimeMs":85206782,"startedAt":"2026-08-08T16:04:58.488Z"},{"name":"abramsego","status":"online","cpu":4,"memMB":696,"restarts":0,"uptimeMs":85206239,"startedAt":"2026-08-08T16:04:59.031Z"},{"name":"pattern-vault","status":"online","cpu":0,"memMB":68,"restarts":0,"uptimeMs":85206239,"startedAt":"2026-08-08T16:04:59.031Z"},{"name":"cabinet-mindmap","status":"online","cpu":0,"memMB":71,"restarts":0,"uptimeMs":85206230,"startedAt":"2026-08-08T16:04:59.040Z"},{"name":"patterndesignlab","status":"online","cpu":0,"memMB":68,"restarts":0,"uptimeMs":85206225,"startedAt":"2026-08-08T16:04:59.045Z"},{"name":"japan-viewer","status":"online","cpu":0,"memMB":462,"restarts":0,"uptimeMs":85206223,"startedAt":"2026-08-08T16:04:59.047Z"},{"name":"reidwitlin-landing","status":"online","cpu":0,"memMB":69,"restarts":0,"uptimeMs":85206196,"startedAt":"2026-08-08T16:04:59.074Z"},{"name":"abramsos","status":"online","cpu":0.5,"memMB":110,"restarts":0,"uptimeMs":85206195,"startedAt":"2026-08-08T16:04:59.075Z"},{"name":"dw-pitch-followup","status":"online","cpu":0,"memMB":82,"restarts":0,"uptimeMs":85206175,"startedAt":"2026-08-08T16:04:59.095Z"},{"name":"wpb-sales-dashboard","status":"online","cpu":0.5,"memMB":65,"restarts":0,"uptimeMs":85206170,"startedAt":"2026-08-08T16:04:59.100Z"},{"name":"crezana-internal","status":"online","cpu":0,"memMB":73,"restarts":0,"uptimeMs":85206110,"startedAt":"2026-08-08T16:04:59.160Z"},{"name":"fromental-internal","status":"online","cpu":0,"memMB":82,"restarts":0,"uptimeMs":85206106,"startedAt":"2026-08-08T16:04:59.164Z"},{"name":"gracie-internal","status":"online","cpu":0.5,"memMB":80,"restarts":0,"uptimeMs":85206061,"startedAt":"2026-08-08T16:04:59.209Z"},{"name":"zuber-internal","status":"online","cpu":0,"memMB":82,"restarts":0,"uptimeMs":85206060,"startedAt":"2026-08-08T16:04:59.210Z"},{"name":"wpb-control-panel","status":"online","cpu":0.5,"memMB":70,"restarts":0,"uptimeMs":85206029,"startedAt":"2026-08-08T16:04:59.241Z"},{"name":"ken","status":"online","cpu":0,"memMB":414,"restarts":0,"uptimeMs":85205974,"startedAt":"2026-08-08T16:04:59.296Z"},{"name":"slack-idea-board","status":"online","cpu":0,"memMB":159,"restarts":0,"uptimeMs":85205896,"startedAt":"2026-08-08T16:04:59.374Z"},{"name":"slack-to-steve-dash","status":"online","cpu":0,"memMB":70,"restarts":0,"uptimeMs":85205896,"startedAt":"2026-08-08T16:04:59.374Z"},{"name":"ImportNewSkufromURL-9830","status":"online","cpu":0,"memMB":73,"restarts":0,"uptimeMs":85205835,"startedAt":"2026-08-08T16:04:59.435Z"},{"name":"govarbitrage","status":"online","cpu":0,"memMB":48,"restarts":0,"uptimeMs":85205834,"startedAt":"2026-08-08T16:04:59.436Z"},{"name":"greenland-internal","status":"online","cpu":0,"memMB":68,"restarts":0,"uptimeMs":85205812,"startedAt":"2026-08-08T16:04:59.458Z"},{"name":"slack-socket","status":"online","cpu":0,"memMB":77,"restarts":0,"uptimeMs":85205811,"startedAt":"2026-08-08T16:04:59.459Z"},{"name":"dw-activation-calendar","status":"online","cpu":0,"memMB":71,"restarts":0,"uptimeMs":85205794,"startedAt":"2026-08-08T16:04:59.476Z"},{"name":"ai-workforce","status":"online","cpu":0,"memMB":70,"restarts":0,"uptimeMs":85205794,"startedAt":"2026-08-08T16:04:59.476Z"},{"name":"prev-estimate-instant","status":"online","cpu":0.5,"memMB":68,"restarts":0,"uptimeMs":85205770,"startedAt":"2026-08-08T16:04:59.500Z"},{"name":"gate-estimate-instant","status":"online","cpu":0.5,"memMB":70,"restarts":0,"uptimeMs":85205769,"startedAt":"2026-08-08T16:04:59.501Z"},{"name":"prev-trade-portal","status":"online","cpu":0,"memMB":69,"restarts":0,"uptimeMs":85205746,"startedAt":"2026-08-08T16:04:59.524Z"},{"name":"gate-trade-portal","status":"online","cpu":0,"memMB":70,"restarts":0,"uptimeMs":85205745,"startedAt":"2026-08-08T16:04:59.525Z"},{"name":"prev-sample-box","status":"online","cpu":0.5,"memMB":70,"restarts":0,"uptimeMs":85205700,"startedAt":"2026-08-08T16:04:59.570Z"},{"name":"gate-sample-box","status":"online","cpu":0,"memMB":70,"restarts":0,"uptimeMs":85205672,"startedAt":"2026-08-08T16:04:59.598Z"},{"name":"prev-reno-visualizer","status":"online","cpu":0,"memMB":66,"restarts":0,"uptimeMs":85205667,"startedAt":"2026-08-08T16:04:59.603Z"},{"name":"gate-reno-visualizer","status":"online","cpu":0,"memMB":70,"restarts":0,"uptimeMs":85205643,"startedAt":"2026-08-08T16:04:59.627Z"},{"name":"prev-permit-radar","status":"online","cpu":0.5,"memMB":68,"restarts":0,"uptimeMs":85205643,"startedAt":"2026-08-08T16:04:59.627Z"},{"name":"gate-permit-radar","status":"online","cpu":0,"memMB":70,"restarts":0,"uptimeMs":85205572,"startedAt":"2026-08-08T16:04:59.698Z"},{"name":"prev-accelerator","status":"online","cpu":0,"memMB":68,"restarts":0,"uptimeMs":85205554,"startedAt":"2026-08-08T16:04:59.716Z"},{"name":"gate-accelerator","status":"online","cpu":0.5,"memMB":70,"restarts":0,"uptimeMs":85205536,"startedAt":"2026-08-08T16:04:59.734Z"},{"name":"schumacher-internal","status":"online","cpu":0,"memMB":84,"restarts":0,"uptimeMs":85205494,"startedAt":"2026-08-08T16:04:59.776Z"},{"name":"colorwheel","status":"online","cpu":0,"memMB":64,"restarts":0,"uptimeMs":85205492,"startedAt":"2026-08-08T16:04:59.778Z"},{"name":"fantasea-consulting","status":"online","cpu":0,"memMB":69,"restarts":0,"uptimeMs":85205439,"startedAt":"2026-08-08T16:04:59.831Z"},{"name":"animals-viewer","status":"online","cpu":0.5,"memMB":85,"restarts":0,"uptimeMs":85205394,"startedAt":"2026-08-08T16:04:59.876Z"},{"name":"consulting-intake","status":"online","cpu":0,"memMB":68,"restarts":0,"uptimeMs":85205394,"startedAt":"2026-08-08T16:04:59.876Z"},{"name":"dw-marketing-reels-studio","status":"online","cpu":0,"memMB":64,"restarts":0,"uptimeMs":85205293,"startedAt":"2026-08-08T16:04:59.977Z"},{"name":"usrealestate","status":"online","cpu":0,"memMB":48,"restarts":0,"uptimeMs":85205293,"startedAt":"2026-08-08T16:04:59.977Z"},{"name":"usre-broker-rotate","status":"stopped","cpu":0,"memMB":0,"restarts":0,"uptimeMs":161663,"startedAt":"2026-08-09T15:42:23.607Z"},{"name":"prestige-car-wash","status":"online","cpu":0,"memMB":75,"restarts":0,"uptimeMs":85205225,"startedAt":"2026-08-08T16:05:00.045Z"},{"name":"dw-cadence-next2","status":"online","cpu":0.5,"memMB":77,"restarts":0,"uptimeMs":85205155,"startedAt":"2026-08-08T16:05:00.115Z"},{"name":"rigo-line-viewer","status":"online","cpu":0,"memMB":81,"restarts":115,"uptimeMs":85182408,"startedAt":"2026-08-08T16:05:22.862Z"},{"name":"fentucci-viewer","status":"online","cpu":0,"memMB":79,"restarts":112,"uptimeMs":85182521,"startedAt":"2026-08-08T16:05:22.749Z"},{"name":"agentabrams-viewer","status":"online","cpu":0,"memMB":67,"restarts":0,"uptimeMs":85205127,"startedAt":"2026-08-08T16:05:00.143Z"},{"name":"model-arena","status":"online","cpu":0,"memMB":93,"restarts":0,"uptimeMs":85205048,"startedAt":"2026-08-08T16:05:00.222Z"},{"name":"dw-domain-viewer","status":"online","cpu":0,"memMB":68,"restarts":0,"uptimeMs":85205022,"startedAt":"2026-08-08T16:05:00.248Z"},{"name":"whimsical-viewer","status":"online","cpu":0,"memMB":70,"restarts":0,"uptimeMs":85205003,"startedAt":"2026-08-08T16:05:00.267Z"},{"name":"dw-internal-gateway","status":"online","cpu":0.5,"memMB":72,"restarts":0,"uptimeMs":85204993,"startedAt":"2026-08-08T16:05:00.277Z"},{"name":"internal-dw-dns","status":"online","cpu":0,"memMB":2,"restarts":552,"uptimeMs":98747,"startedAt":"2026-08-09T15:43:26.523Z"},{"name":"all-dw-local","status":"online","cpu":0,"memMB":1802,"restarts":92,"uptimeMs":85182513,"startedAt":"2026-08-08T16:05:22.757Z"},{"name":"vahallan-line-viewer","status":"online","cpu":0,"memMB":79,"restarts":117,"uptimeMs":85182371,"startedAt":"2026-08-08T16:05:22.899Z"},{"name":"clipreviewbydave","status":"online","cpu":0.5,"memMB":70,"restarts":0,"uptimeMs":85204933,"startedAt":"2026-08-08T16:05:00.337Z"},{"name":"ticket-board","status":"online","cpu":0.5,"memMB":96,"restarts":0,"uptimeMs":85204924,"startedAt":"2026-08-08T16:05:00.346Z"},{"name":"muralsource-internal","status":"online","cpu":0,"memMB":79,"restarts":0,"uptimeMs":85204828,"startedAt":"2026-08-08T16:05:00.442Z"},{"name":"projects-agentabrams","status":"online","cpu":0.5,"memMB":67,"restarts":0,"uptimeMs":85204826,"startedAt":"2026-08-08T16:05:00.444Z"},{"name":"build-pages","status":"online","cpu":0.5,"memMB":154,"restarts":0,"uptimeMs":85204782,"startedAt":"2026-08-08T16:05:00.488Z"},{"name":"approvals-viewer","status":"online","cpu":0,"memMB":69,"restarts":0,"uptimeMs":85204714,"startedAt":"2026-08-08T16:05:00.556Z"},{"name":"hos-feed-api","status":"online","cpu":0,"memMB":63,"restarts":0,"uptimeMs":85204711,"startedAt":"2026-08-08T16:05:00.559Z"},{"name":"hos-permit-viewer","status":"online","cpu":0,"memMB":63,"restarts":0,"uptimeMs":85204689,"startedAt":"2026-08-08T16:05:00.581Z"},{"name":"hos-engine","status":"online","cpu":0,"memMB":63,"restarts":0,"uptimeMs":85204687,"startedAt":"2026-08-08T16:05:00.583Z"},{"name":"usre-rentv-feed","status":"online","cpu":0.5,"memMB":62,"restarts":0,"uptimeMs":85204675,"startedAt":"2026-08-08T16:05:00.595Z"},{"name":"approvals-rollup","status":"online","cpu":0.5,"memMB":70,"restarts":0,"uptimeMs":85204670,"startedAt":"2026-08-08T16:05:00.600Z"},{"name":"dw-signup-fulfillment","status":"online","cpu":0,"memMB":68,"restarts":0,"uptimeMs":85204665,"startedAt":"2026-08-08T16:05:00.605Z"},{"name":"rentv","status":"online","cpu":0,"memMB":71,"restarts":0,"uptimeMs":85204492,"startedAt":"2026-08-08T16:05:00.778Z"},{"name":"tools-dw-hub","status":"online","cpu":0.5,"memMB":70,"restarts":0,"uptimeMs":85204465,"startedAt":"2026-08-08T16:05:00.805Z"},{"name":"new-engine","status":"online","cpu":0.5,"memMB":66,"restarts":0,"uptimeMs":85204431,"startedAt":"2026-08-08T16:05:00.839Z"},{"name":"hos-web","status":"online","cpu":0,"memMB":46,"restarts":0,"uptimeMs":85204428,"startedAt":"2026-08-08T16:05:00.842Z"},{"name":"hos-admin","status":"online","cpu":0,"memMB":46,"restarts":0,"uptimeMs":85204394,"startedAt":"2026-08-08T16:05:00.876Z"},{"name":"corvette-console","status":"online","cpu":0,"memMB":69,"restarts":0,"uptimeMs":85204391,"startedAt":"2026-08-08T16:05:00.879Z"},{"name":"dwtool-150dpi","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":85204386,"startedAt":"2026-08-08T16:05:00.884Z"},{"name":"dwtool-recolor-v14","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":85204349,"startedAt":"2026-08-08T16:05:00.921Z"},{"name":"dwtool-image-to-room","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":85204338,"startedAt":"2026-08-08T16:05:00.932Z"},{"name":"dwtool-room-setting-app","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":85204331,"startedAt":"2026-08-08T16:05:00.939Z"},{"name":"dwtool-color-wheel","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":85204329,"startedAt":"2026-08-08T16:05:00.941Z"},{"name":"dwtool-tagger-kravet","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":85204324,"startedAt":"2026-08-08T16:05:00.946Z"},{"name":"dwtool-tagger-thibaut","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":85204323,"startedAt":"2026-08-08T16:05:00.947Z"},{"name":"dwtool-tagger-pr","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":85204319,"startedAt":"2026-08-08T16:05:00.951Z"},{"name":"dwtool-updater-maya","status":"online","cpu":0,"memMB":1,"restarts":61,"uptimeMs":85182550,"startedAt":"2026-08-08T16:05:22.720Z"},{"name":"dwtool-vendor-dashboard-v2","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":85204312,"startedAt":"2026-08-08T16:05:00.958Z"},{"name":"dwtool-calendar","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":85204307,"startedAt":"2026-08-08T16:05:00.963Z"},{"name":"dwtool-master-listings","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":85204305,"startedAt":"2026-08-08T16:05:00.965Z"},{"name":"dwtool-system-dashboard","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":85204297,"startedAt":"2026-08-08T16:05:00.973Z"},{"name":"dwtool-pm2-dashboard","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":85204289,"startedAt":"2026-08-08T16:05:00.981Z"},{"name":"dwtool-recolor-v13","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":85204286,"startedAt":"2026-08-08T16:05:00.984Z"},{"name":"dwtool-recolor-v1","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":85204266,"startedAt":"2026-08-08T16:05:01.004Z"},{"name":"dwtool-import-sku","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":85204261,"startedAt":"2026-08-08T16:05:01.009Z"},{"name":"dwtool-updater-schumacher","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":85204251,"startedAt":"2026-08-08T16:05:01.019Z"},{"name":"dwtool-updater-architectural","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":85204251,"startedAt":"2026-08-08T16:05:01.019Z"},{"name":"dwtool-sku-to-blog","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":85204242,"startedAt":"2026-08-08T16:05:01.028Z"},{"name":"dwtool-settlement-review","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":85204242,"startedAt":"2026-08-08T16:05:01.028Z"},{"name":"dwtool-dw-dashboard","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":85204235,"startedAt":"2026-08-08T16:05:01.035Z"},{"name":"ticket-triage","status":"online","cpu":0,"memMB":71,"restarts":0,"uptimeMs":85204234,"startedAt":"2026-08-08T16:05:01.036Z"},{"name":"vendor-command-center","status":"online","cpu":0.5,"memMB":91,"restarts":0,"uptimeMs":85203994,"startedAt":"2026-08-08T16:05:01.276Z"},{"name":"interiordesignershowroom","status":"online","cpu":0.5,"memMB":85,"restarts":0,"uptimeMs":85203972,"startedAt":"2026-08-08T16:05:01.298Z"},{"name":"waitline","status":"online","cpu":0,"memMB":70,"restarts":0,"uptimeMs":85203887,"startedAt":"2026-08-08T16:05:01.383Z"},{"name":"agent-ad-network","status":"online","cpu":0,"memMB":70,"restarts":0,"uptimeMs":85203880,"startedAt":"2026-08-08T16:05:01.390Z"},{"name":"ken-trade-siren","status":"online","cpu":0,"memMB":71,"restarts":0,"uptimeMs":85203875,"startedAt":"2026-08-08T16:05:01.395Z"},{"name":"ga-allsites","status":"online","cpu":0,"memMB":17,"restarts":0,"uptimeMs":85203826,"startedAt":"2026-08-08T16:05:01.444Z"},{"name":"ga4-dashboard","status":"online","cpu":0,"memMB":69,"restarts":0,"uptimeMs":85203778,"startedAt":"2026-08-08T16:05:01.492Z"},{"name":"norma-instagram","status":"online","cpu":0,"memMB":85,"restarts":0,"uptimeMs":85203696,"startedAt":"2026-08-08T16:05:01.574Z"},{"name":"nodailyworries-local","status":"online","cpu":0,"memMB":67,"restarts":0,"uptimeMs":85203694,"startedAt":"2026-08-08T16:05:01.576Z"},{"name":"norma-email","status":"online","cpu":0.5,"memMB":57,"restarts":0,"uptimeMs":85203546,"startedAt":"2026-08-08T16:05:01.724Z"},{"name":"calendar-agentabrams","status":"online","cpu":0.5,"memMB":70,"restarts":0,"uptimeMs":85203544,"startedAt":"2026-08-08T16:05:01.726Z"},{"name":"thedesignerlibrary","status":"online","cpu":0,"memMB":70,"restarts":0,"uptimeMs":85203474,"startedAt":"2026-08-08T16:05:01.796Z"},{"name":"ventura-corridor","status":"online","cpu":0,"memMB":56,"restarts":0,"uptimeMs":85203473,"startedAt":"2026-08-08T16:05:01.797Z"},{"name":"rentv-ad-engine","status":"online","cpu":0,"memMB":70,"restarts":0,"uptimeMs":85203388,"startedAt":"2026-08-08T16:05:01.882Z"},{"name":"rentv2026-preview","status":"online","cpu":0,"memMB":73,"restarts":0,"uptimeMs":85203387,"startedAt":"2026-08-08T16:05:01.883Z"}]}
\ No newline at end of file
+{"fetchedAt":"2026-08-09T16:15:50.548Z","procs":[{"name":"pm2-logrotate","status":"online","cpu":0.2,"memMB":84,"restarts":0,"uptimeMs":87052060,"startedAt":"2026-08-08T16:04:58.488Z"},{"name":"abramsego","status":"online","cpu":1.2,"memMB":696,"restarts":2,"uptimeMs":30467,"startedAt":"2026-08-09T16:15:20.081Z"},{"name":"pattern-vault","status":"online","cpu":0.2,"memMB":68,"restarts":0,"uptimeMs":87051517,"startedAt":"2026-08-08T16:04:59.031Z"},{"name":"cabinet-mindmap","status":"online","cpu":0.2,"memMB":71,"restarts":0,"uptimeMs":87051508,"startedAt":"2026-08-08T16:04:59.040Z"},{"name":"patterndesignlab","status":"online","cpu":0.2,"memMB":69,"restarts":0,"uptimeMs":87051503,"startedAt":"2026-08-08T16:04:59.045Z"},{"name":"japan-viewer","status":"online","cpu":0.3,"memMB":391,"restarts":0,"uptimeMs":87051501,"startedAt":"2026-08-08T16:04:59.047Z"},{"name":"reidwitlin-landing","status":"online","cpu":0.2,"memMB":69,"restarts":0,"uptimeMs":87051474,"startedAt":"2026-08-08T16:04:59.074Z"},{"name":"abramsos","status":"online","cpu":0.2,"memMB":110,"restarts":0,"uptimeMs":87051473,"startedAt":"2026-08-08T16:04:59.075Z"},{"name":"dw-pitch-followup","status":"online","cpu":0.2,"memMB":82,"restarts":0,"uptimeMs":87051453,"startedAt":"2026-08-08T16:04:59.095Z"},{"name":"wpb-sales-dashboard","status":"online","cpu":0.2,"memMB":65,"restarts":0,"uptimeMs":87051448,"startedAt":"2026-08-08T16:04:59.100Z"},{"name":"crezana-internal","status":"online","cpu":0.2,"memMB":73,"restarts":0,"uptimeMs":87051388,"startedAt":"2026-08-08T16:04:59.160Z"},{"name":"fromental-internal","status":"online","cpu":0.2,"memMB":78,"restarts":0,"uptimeMs":87051384,"startedAt":"2026-08-08T16:04:59.164Z"},{"name":"gracie-internal","status":"online","cpu":0.2,"memMB":81,"restarts":0,"uptimeMs":87051339,"startedAt":"2026-08-08T16:04:59.209Z"},{"name":"zuber-internal","status":"online","cpu":0.2,"memMB":82,"restarts":0,"uptimeMs":87051338,"startedAt":"2026-08-08T16:04:59.210Z"},{"name":"wpb-control-panel","status":"online","cpu":0.2,"memMB":65,"restarts":0,"uptimeMs":87051307,"startedAt":"2026-08-08T16:04:59.241Z"},{"name":"ken","status":"online","cpu":0,"memMB":439,"restarts":0,"uptimeMs":87051252,"startedAt":"2026-08-08T16:04:59.296Z"},{"name":"slack-idea-board","status":"online","cpu":0.7,"memMB":145,"restarts":0,"uptimeMs":87051174,"startedAt":"2026-08-08T16:04:59.374Z"},{"name":"slack-to-steve-dash","status":"online","cpu":0.2,"memMB":64,"restarts":0,"uptimeMs":87051174,"startedAt":"2026-08-08T16:04:59.374Z"},{"name":"ImportNewSkufromURL-9830","status":"online","cpu":0.2,"memMB":73,"restarts":0,"uptimeMs":87051113,"startedAt":"2026-08-08T16:04:59.435Z"},{"name":"govarbitrage","status":"online","cpu":0,"memMB":48,"restarts":0,"uptimeMs":87051112,"startedAt":"2026-08-08T16:04:59.436Z"},{"name":"greenland-internal","status":"online","cpu":0,"memMB":69,"restarts":0,"uptimeMs":87051090,"startedAt":"2026-08-08T16:04:59.458Z"},{"name":"slack-socket","status":"online","cpu":0.2,"memMB":77,"restarts":0,"uptimeMs":87051089,"startedAt":"2026-08-08T16:04:59.459Z"},{"name":"dw-activation-calendar","status":"online","cpu":0.2,"memMB":71,"restarts":0,"uptimeMs":87051072,"startedAt":"2026-08-08T16:04:59.476Z"},{"name":"ai-workforce","status":"online","cpu":0.2,"memMB":65,"restarts":0,"uptimeMs":87051072,"startedAt":"2026-08-08T16:04:59.476Z"},{"name":"prev-estimate-instant","status":"online","cpu":0.2,"memMB":69,"restarts":0,"uptimeMs":87051048,"startedAt":"2026-08-08T16:04:59.500Z"},{"name":"gate-estimate-instant","status":"online","cpu":0.2,"memMB":65,"restarts":0,"uptimeMs":87051047,"startedAt":"2026-08-08T16:04:59.501Z"},{"name":"prev-trade-portal","status":"online","cpu":0.2,"memMB":69,"restarts":0,"uptimeMs":87051024,"startedAt":"2026-08-08T16:04:59.524Z"},{"name":"gate-trade-portal","status":"online","cpu":0.2,"memMB":65,"restarts":0,"uptimeMs":87051023,"startedAt":"2026-08-08T16:04:59.525Z"},{"name":"prev-sample-box","status":"online","cpu":0.2,"memMB":71,"restarts":0,"uptimeMs":87050978,"startedAt":"2026-08-08T16:04:59.570Z"},{"name":"gate-sample-box","status":"online","cpu":0.2,"memMB":65,"restarts":0,"uptimeMs":87050950,"startedAt":"2026-08-08T16:04:59.598Z"},{"name":"prev-reno-visualizer","status":"online","cpu":0.2,"memMB":66,"restarts":0,"uptimeMs":87050945,"startedAt":"2026-08-08T16:04:59.603Z"},{"name":"gate-reno-visualizer","status":"online","cpu":0.3,"memMB":65,"restarts":0,"uptimeMs":87050921,"startedAt":"2026-08-08T16:04:59.627Z"},{"name":"prev-permit-radar","status":"online","cpu":0.2,"memMB":69,"restarts":0,"uptimeMs":87050921,"startedAt":"2026-08-08T16:04:59.627Z"},{"name":"gate-permit-radar","status":"online","cpu":0.2,"memMB":65,"restarts":0,"uptimeMs":87050850,"startedAt":"2026-08-08T16:04:59.698Z"},{"name":"prev-accelerator","status":"online","cpu":0.2,"memMB":69,"restarts":0,"uptimeMs":87050832,"startedAt":"2026-08-08T16:04:59.716Z"},{"name":"gate-accelerator","status":"online","cpu":0.2,"memMB":65,"restarts":0,"uptimeMs":87050814,"startedAt":"2026-08-08T16:04:59.734Z"},{"name":"schumacher-internal","status":"online","cpu":0.2,"memMB":84,"restarts":0,"uptimeMs":87050772,"startedAt":"2026-08-08T16:04:59.776Z"},{"name":"colorwheel","status":"online","cpu":0.2,"memMB":64,"restarts":0,"uptimeMs":87050770,"startedAt":"2026-08-08T16:04:59.778Z"},{"name":"fantasea-consulting","status":"online","cpu":0.2,"memMB":70,"restarts":0,"uptimeMs":87050717,"startedAt":"2026-08-08T16:04:59.831Z"},{"name":"animals-viewer","status":"online","cpu":0.3,"memMB":85,"restarts":0,"uptimeMs":87050672,"startedAt":"2026-08-08T16:04:59.876Z"},{"name":"consulting-intake","status":"online","cpu":0,"memMB":68,"restarts":0,"uptimeMs":87050672,"startedAt":"2026-08-08T16:04:59.876Z"},{"name":"dw-marketing-reels-studio","status":"online","cpu":0.2,"memMB":64,"restarts":0,"uptimeMs":87050571,"startedAt":"2026-08-08T16:04:59.977Z"},{"name":"usrealestate","status":"online","cpu":0,"memMB":48,"restarts":0,"uptimeMs":87050571,"startedAt":"2026-08-08T16:04:59.977Z"},{"name":"usre-broker-rotate","status":"stopped","cpu":0,"memMB":0,"restarts":0,"uptimeMs":155823,"startedAt":"2026-08-09T16:13:14.725Z"},{"name":"prestige-car-wash","status":"online","cpu":0.2,"memMB":76,"restarts":0,"uptimeMs":87050503,"startedAt":"2026-08-08T16:05:00.045Z"},{"name":"dw-cadence-next2","status":"online","cpu":0.2,"memMB":81,"restarts":0,"uptimeMs":87050433,"startedAt":"2026-08-08T16:05:00.115Z"},{"name":"rigo-line-viewer","status":"online","cpu":0.2,"memMB":81,"restarts":115,"uptimeMs":87027686,"startedAt":"2026-08-08T16:05:22.862Z"},{"name":"fentucci-viewer","status":"online","cpu":0,"memMB":79,"restarts":112,"uptimeMs":87027799,"startedAt":"2026-08-08T16:05:22.749Z"},{"name":"agentabrams-viewer","status":"online","cpu":0.2,"memMB":68,"restarts":0,"uptimeMs":87050405,"startedAt":"2026-08-08T16:05:00.143Z"},{"name":"model-arena","status":"online","cpu":0.2,"memMB":79,"restarts":0,"uptimeMs":87050326,"startedAt":"2026-08-08T16:05:00.222Z"},{"name":"dw-domain-viewer","status":"online","cpu":0,"memMB":68,"restarts":0,"uptimeMs":87050300,"startedAt":"2026-08-08T16:05:00.248Z"},{"name":"whimsical-viewer","status":"online","cpu":0.2,"memMB":66,"restarts":0,"uptimeMs":87050281,"startedAt":"2026-08-08T16:05:00.267Z"},{"name":"dw-internal-gateway","status":"online","cpu":0.2,"memMB":67,"restarts":0,"uptimeMs":87050271,"startedAt":"2026-08-08T16:05:00.277Z"},{"name":"internal-dw-dns","status":"online","cpu":0,"memMB":2,"restarts":564,"uptimeMs":92903,"startedAt":"2026-08-09T16:14:17.645Z"},{"name":"all-dw-local","status":"online","cpu":18.5,"memMB":2297,"restarts":92,"uptimeMs":87027791,"startedAt":"2026-08-08T16:05:22.757Z"},{"name":"vahallan-line-viewer","status":"online","cpu":0.2,"memMB":80,"restarts":117,"uptimeMs":87027649,"startedAt":"2026-08-08T16:05:22.899Z"},{"name":"clipreviewbydave","status":"online","cpu":0.2,"memMB":67,"restarts":0,"uptimeMs":87050211,"startedAt":"2026-08-08T16:05:00.337Z"},{"name":"ticket-board","status":"online","cpu":0.3,"memMB":100,"restarts":0,"uptimeMs":87050202,"startedAt":"2026-08-08T16:05:00.346Z"},{"name":"muralsource-internal","status":"online","cpu":0.2,"memMB":79,"restarts":0,"uptimeMs":87050106,"startedAt":"2026-08-08T16:05:00.442Z"},{"name":"projects-agentabrams","status":"online","cpu":0,"memMB":67,"restarts":0,"uptimeMs":87050104,"startedAt":"2026-08-08T16:05:00.444Z"},{"name":"build-pages","status":"online","cpu":0.2,"memMB":156,"restarts":0,"uptimeMs":87050060,"startedAt":"2026-08-08T16:05:00.488Z"},{"name":"approvals-viewer","status":"online","cpu":0.2,"memMB":65,"restarts":0,"uptimeMs":87049992,"startedAt":"2026-08-08T16:05:00.556Z"},{"name":"hos-feed-api","status":"online","cpu":0.2,"memMB":64,"restarts":0,"uptimeMs":87049989,"startedAt":"2026-08-08T16:05:00.559Z"},{"name":"hos-permit-viewer","status":"online","cpu":0,"memMB":64,"restarts":0,"uptimeMs":87049967,"startedAt":"2026-08-08T16:05:00.581Z"},{"name":"hos-engine","status":"online","cpu":0,"memMB":64,"restarts":0,"uptimeMs":87049965,"startedAt":"2026-08-08T16:05:00.583Z"},{"name":"usre-rentv-feed","status":"online","cpu":0,"memMB":62,"restarts":0,"uptimeMs":87049953,"startedAt":"2026-08-08T16:05:00.595Z"},{"name":"approvals-rollup","status":"online","cpu":0.2,"memMB":70,"restarts":0,"uptimeMs":87049948,"startedAt":"2026-08-08T16:05:00.600Z"},{"name":"dw-signup-fulfillment","status":"online","cpu":0.2,"memMB":69,"restarts":0,"uptimeMs":87049943,"startedAt":"2026-08-08T16:05:00.605Z"},{"name":"rentv","status":"online","cpu":0.2,"memMB":71,"restarts":0,"uptimeMs":87049770,"startedAt":"2026-08-08T16:05:00.778Z"},{"name":"tools-dw-hub","status":"online","cpu":0,"memMB":70,"restarts":0,"uptimeMs":87049743,"startedAt":"2026-08-08T16:05:00.805Z"},{"name":"new-engine","status":"online","cpu":0,"memMB":66,"restarts":0,"uptimeMs":87049709,"startedAt":"2026-08-08T16:05:00.839Z"},{"name":"hos-web","status":"online","cpu":0,"memMB":46,"restarts":0,"uptimeMs":87049706,"startedAt":"2026-08-08T16:05:00.842Z"},{"name":"hos-admin","status":"online","cpu":0,"memMB":46,"restarts":0,"uptimeMs":87049672,"startedAt":"2026-08-08T16:05:00.876Z"},{"name":"corvette-console","status":"online","cpu":0.2,"memMB":73,"restarts":0,"uptimeMs":87049669,"startedAt":"2026-08-08T16:05:00.879Z"},{"name":"dwtool-150dpi","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":87049664,"startedAt":"2026-08-08T16:05:00.884Z"},{"name":"dwtool-recolor-v14","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":87049627,"startedAt":"2026-08-08T16:05:00.921Z"},{"name":"dwtool-image-to-room","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":87049616,"startedAt":"2026-08-08T16:05:00.932Z"},{"name":"dwtool-room-setting-app","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":87049609,"startedAt":"2026-08-08T16:05:00.939Z"},{"name":"dwtool-color-wheel","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":87049607,"startedAt":"2026-08-08T16:05:00.941Z"},{"name":"dwtool-tagger-kravet","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":87049602,"startedAt":"2026-08-08T16:05:00.946Z"},{"name":"dwtool-tagger-thibaut","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":87049601,"startedAt":"2026-08-08T16:05:00.947Z"},{"name":"dwtool-tagger-pr","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":87049597,"startedAt":"2026-08-08T16:05:00.951Z"},{"name":"dwtool-updater-maya","status":"online","cpu":0,"memMB":1,"restarts":61,"uptimeMs":87027828,"startedAt":"2026-08-08T16:05:22.720Z"},{"name":"dwtool-vendor-dashboard-v2","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":87049590,"startedAt":"2026-08-08T16:05:00.958Z"},{"name":"dwtool-calendar","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":87049585,"startedAt":"2026-08-08T16:05:00.963Z"},{"name":"dwtool-master-listings","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":87049583,"startedAt":"2026-08-08T16:05:00.965Z"},{"name":"dwtool-system-dashboard","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":87049575,"startedAt":"2026-08-08T16:05:00.973Z"},{"name":"dwtool-pm2-dashboard","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":87049567,"startedAt":"2026-08-08T16:05:00.981Z"},{"name":"dwtool-recolor-v13","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":87049564,"startedAt":"2026-08-08T16:05:00.984Z"},{"name":"dwtool-recolor-v1","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":87049544,"startedAt":"2026-08-08T16:05:01.004Z"},{"name":"dwtool-import-sku","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":87049539,"startedAt":"2026-08-08T16:05:01.009Z"},{"name":"dwtool-updater-schumacher","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":87049529,"startedAt":"2026-08-08T16:05:01.019Z"},{"name":"dwtool-updater-architectural","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":87049529,"startedAt":"2026-08-08T16:05:01.019Z"},{"name":"dwtool-sku-to-blog","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":87049520,"startedAt":"2026-08-08T16:05:01.028Z"},{"name":"dwtool-settlement-review","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":87049520,"startedAt":"2026-08-08T16:05:01.028Z"},{"name":"dwtool-dw-dashboard","status":"online","cpu":0,"memMB":1,"restarts":0,"uptimeMs":87049513,"startedAt":"2026-08-08T16:05:01.035Z"},{"name":"ticket-triage","status":"online","cpu":0.2,"memMB":70,"restarts":0,"uptimeMs":87049512,"startedAt":"2026-08-08T16:05:01.036Z"},{"name":"vendor-command-center","status":"online","cpu":0,"memMB":91,"restarts":0,"uptimeMs":87049272,"startedAt":"2026-08-08T16:05:01.276Z"},{"name":"interiordesignershowroom","status":"online","cpu":0.2,"memMB":85,"restarts":0,"uptimeMs":87049250,"startedAt":"2026-08-08T16:05:01.298Z"},{"name":"waitline","status":"online","cpu":0.2,"memMB":70,"restarts":0,"uptimeMs":87049165,"startedAt":"2026-08-08T16:05:01.383Z"},{"name":"agent-ad-network","status":"online","cpu":0.2,"memMB":70,"restarts":0,"uptimeMs":87049158,"startedAt":"2026-08-08T16:05:01.390Z"},{"name":"ken-trade-siren","status":"online","cpu":0.2,"memMB":71,"restarts":0,"uptimeMs":87049153,"startedAt":"2026-08-08T16:05:01.395Z"},{"name":"ga-allsites","status":"online","cpu":0,"memMB":17,"restarts":0,"uptimeMs":87049104,"startedAt":"2026-08-08T16:05:01.444Z"},{"name":"ga4-dashboard","status":"online","cpu":0.2,"memMB":70,"restarts":0,"uptimeMs":87049056,"startedAt":"2026-08-08T16:05:01.492Z"},{"name":"norma-instagram","status":"online","cpu":0,"memMB":88,"restarts":0,"uptimeMs":87048974,"startedAt":"2026-08-08T16:05:01.574Z"},{"name":"nodailyworries-local","status":"online","cpu":0.2,"memMB":68,"restarts":0,"uptimeMs":87048972,"startedAt":"2026-08-08T16:05:01.576Z"},{"name":"norma-email","status":"online","cpu":0.2,"memMB":57,"restarts":0,"uptimeMs":87048824,"startedAt":"2026-08-08T16:05:01.724Z"},{"name":"calendar-agentabrams","status":"online","cpu":0.2,"memMB":70,"restarts":0,"uptimeMs":87048822,"startedAt":"2026-08-08T16:05:01.726Z"},{"name":"thedesignerlibrary","status":"online","cpu":0.2,"memMB":70,"restarts":0,"uptimeMs":87048752,"startedAt":"2026-08-08T16:05:01.796Z"},{"name":"ventura-corridor","status":"online","cpu":0,"memMB":56,"restarts":0,"uptimeMs":87048751,"startedAt":"2026-08-08T16:05:01.797Z"},{"name":"rentv-ad-engine","status":"online","cpu":0,"memMB":65,"restarts":0,"uptimeMs":87048666,"startedAt":"2026-08-08T16:05:01.882Z"},{"name":"rentv2026-preview","status":"online","cpu":0.2,"memMB":72,"restarts":0,"uptimeMs":87048665,"startedAt":"2026-08-08T16:05:01.883Z"}]}
\ No newline at end of file
diff --git a/lib/a2a-client.js b/lib/a2a-client.js
index 08d45985..8825facf 100644
--- a/lib/a2a-client.js
+++ b/lib/a2a-client.js
@@ -23,6 +23,8 @@ const TIMEOUT_MS = 15_000;
const MAX_POLL_ATTEMPTS = 8;
const POLL_INTERVAL_MS = 2_000;
const CARD_CACHE_TTL_MS = 60 * 60 * 1000; // 1 h
+const MAX_RESPONSE_BYTES = 1_000_000; // 1 MB — a hostile peer must not OOM us
+const MAX_LOGGED_ANSWER = 20_000; // cap what an untrusted answer writes to disk
// payload linter — fields that must never leave this box
const BANNED_PAYLOAD_PATTERNS = [
@@ -52,6 +54,36 @@ function assertSafeUrl(urlStr) {
if (u.protocol !== 'https:') throw new Error(`a2a: only HTTPS allowed, got ${u.protocol}`);
const port = u.port ? Number(u.port) : 443;
if (port !== 443) throw new Error(`a2a: only port 443 allowed, got ${port}`);
+ // Reject embedded credentials (https://user:pass@host) — a hostile card could
+ // otherwise ship userinfo outbound while still passing the same-host check.
+ if (u.username || u.password) throw new Error('a2a: URL must not contain embedded credentials (userinfo)');
+}
+
+/**
+ * Read a fetch response body as JSON with a hard size cap so a hostile peer can't
+ * OOM the process with a giant body. Rejects on Content-Length over the cap and
+ * also truncates-and-fails if the streamed body exceeds it.
+ */
+async function readJsonCapped(res, maxBytes = MAX_RESPONSE_BYTES) {
+ const len = Number(res.headers.get('content-length') || 0);
+ if (len && len > maxBytes) throw new Error(`a2a: response too large (${len} bytes > ${maxBytes} cap)`);
+ const text = await res.text();
+ if (text.length > maxBytes) throw new Error(`a2a: response body exceeded ${maxBytes} byte cap`);
+ return JSON.parse(text);
+}
+
+/**
+ * Throw if targetUrl's host differs from baseUrl's host. A fetched Agent Card is
+ * UNTRUSTED input: its `url` field could otherwise pivot the RPC call (SSRF) to
+ * any other https:443 host. The RPC endpoint must stay on the allowlisted host.
+ */
+function assertSameHost(baseUrl, targetUrl) {
+ let a, b;
+ try { a = new URL(baseUrl); b = new URL(targetUrl); }
+ catch { throw new Error('a2a: unparseable URL in host check'); }
+ if (a.host !== b.host) {
+ throw new Error(`a2a: agent card url host "${b.host}" != allowlisted host "${a.host}" (blocked card-driven redirect)`);
+ }
}
/** fetch with a hard timeout and no-redirect enforcement. */
@@ -86,7 +118,7 @@ async function fetchAgentCard(baseUrl) {
const cardUrl = `${cacheKey}/.well-known/agent-card.json`;
const res = await fetchSafe(cardUrl, { headers: { Accept: 'application/json' } });
if (!res.ok) throw new Error(`a2a: agent card fetch failed ${res.status} from ${cardUrl}`);
- const card = await res.json();
+ const card = await readJsonCapped(res);
if (!card || typeof card !== 'object') throw new Error('a2a: invalid agent card (not an object)');
if (!card.name || !card.url) throw new Error('a2a: agent card missing name/url fields');
cardCache[cacheKey] = { card, fetchedAt: Date.now() };
@@ -124,6 +156,8 @@ function buildGetTask(taskId) {
*/
async function rpcCall(baseUrl, card, body, authHeader) {
const rpcUrl = card?.url || `${baseUrl.replace(/\/$/, '')}/`;
+ // Card is untrusted: keep the RPC on the allowlisted host (SSRF guard).
+ assertSameHost(baseUrl, rpcUrl);
const headers = { 'Content-Type': 'application/json', Accept: 'application/json' };
if (authHeader) headers['Authorization'] = authHeader;
@@ -134,7 +168,7 @@ async function rpcCall(baseUrl, card, body, authHeader) {
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`a2a: RPC ${body.method} returned ${res.status}`);
- return res.json();
+ return readJsonCapped(res);
}
/** Extract text from a completed Task's artifacts, or from an inline message result. */
@@ -216,10 +250,15 @@ function resolveAgent(name) {
/** Append a consult result to the JSONL log. Fire-and-forget (errors logged, not thrown). */
function logConsult(entry) {
try {
- appendFileSync(CONSULTS_LOG, JSON.stringify({ ...entry, ts: new Date().toISOString() }) + '\n');
+ // Truncate an untrusted answer before it lands on disk (unbounded-log guard).
+ const safe = { ...entry, ts: new Date().toISOString() };
+ if (typeof safe.answer === 'string' && safe.answer.length > MAX_LOGGED_ANSWER) {
+ safe.answer = safe.answer.slice(0, MAX_LOGGED_ANSWER) + `…[truncated ${safe.answer.length - MAX_LOGGED_ANSWER} chars]`;
+ }
+ appendFileSync(CONSULTS_LOG, JSON.stringify(safe) + '\n');
} catch (e) {
console.error('a2a: consult log write failed:', e.message);
}
}
-module.exports = { fetchAgentCard, consult, loadAllowlist, resolveAgent, logConsult, lintPayload, assertSafeUrl };
+module.exports = { fetchAgentCard, consult, loadAllowlist, resolveAgent, logConsult, lintPayload, assertSafeUrl, assertSameHost, readJsonCapped, MAX_RESPONSE_BYTES };
diff --git a/lib/a2a-client.test.js b/lib/a2a-client.test.js
index 65a5989e..34c0a085 100644
--- a/lib/a2a-client.test.js
+++ b/lib/a2a-client.test.js
@@ -6,7 +6,7 @@
const assert = require('assert');
const http = require('http');
-const { fetchAgentCard, consult, lintPayload, assertSafeUrl, loadAllowlist } = require('./a2a-client');
+const { fetchAgentCard, consult, lintPayload, assertSafeUrl, assertSameHost, loadAllowlist } = require('./a2a-client');
// --- helpers -----------------------------------------------------------------
@@ -30,9 +30,28 @@ function makeServer(responses) {
assert.throws(() => assertSafeUrl('http://example.com'), /only HTTPS/);
assert.throws(() => assertSafeUrl('https://example.com:8080'), /only port 443/);
assert.throws(() => assertSafeUrl('ftp://example.com'), /only HTTPS/);
+assert.throws(() => assertSafeUrl('https://user:pass@example.com'), /embedded credentials/); // no userinfo
assertSafeUrl('https://example.com'); // must not throw
assertSafeUrl('https://example.com:443'); // explicit 443 is fine
-console.log(' assertSafeUrl: 5 assertions pass');
+console.log(' assertSafeUrl: 6 assertions pass');
+
+// --- readJsonCapped (OOM guard) ----------------------------------------------
+const { readJsonCapped } = require('./a2a-client');
+(async () => {
+ const tooBigByHeader = { headers: { get: () => String(5_000_000) }, text: async () => '{}' };
+ await assert.rejects(readJsonCapped(tooBigByHeader), /too large/);
+ const okRes = { headers: { get: () => '' }, text: async () => JSON.stringify({ ok: true }) };
+ assert.deepStrictEqual(await readJsonCapped(okRes), { ok: true });
+ console.log(' readJsonCapped: 2 assertions pass');
+})().catch(e => { console.error('readJsonCapped test FAILED:', e.message); process.exit(1); });
+
+// --- assertSameHost (card-driven SSRF guard) ---------------------------------
+// A fetched Agent Card is untrusted; its url must not pivot the RPC off-host.
+assert.throws(() => assertSameHost('https://good.example.com', 'https://evil.example.com/'), /blocked card-driven redirect/);
+assert.throws(() => assertSameHost('https://good.example.com', 'https://good.example.com.evil.com/'), /blocked card-driven redirect/);
+assertSameHost('https://good.example.com', 'https://good.example.com/rpc'); // same host, different path — fine
+assertSameHost('https://good.example.com/', 'https://good.example.com:443/'); // explicit 443 == default
+console.log(' assertSameHost: 4 assertions pass');
// --- lintPayload -------------------------------------------------------------
diff --git a/memos/a2a-egress-rails.md b/memos/a2a-egress-rails.md
new file mode 100644
index 00000000..a1f84ec5
--- /dev/null
+++ b/memos/a2a-egress-rails.md
@@ -0,0 +1,47 @@
+# A2A egress rails + egress-sentinel cross-reference (TK-10381, Phase C)
+
+**Built:** 2026-08-09 · client-only, activation gated (empty allowlist).
+
+## What egress the A2A client can produce
+
+The A2A client (`lib/a2a-client.js`) is the ONLY new outbound-network surface. Its egress
+is deliberately shaped to be **benign** per the `egress-sentinel` skill (born of the
+2026-07-29 credential-stealer, C2 `144.172.92.199:8080`, `/tmp/.e*.sh` dropper):
+
+- **HTTPS + port 443 ONLY.** `assertSafeUrl()` throws on any `http://` or non-443 port.
+ So there is never an outbound connection on a non-web port (the exact shape the sentinel
+ flags as a C2 beacon).
+- **No cross-host redirects.** `fetch(..., { redirect: 'error' })` — a 3xx aborts.
+- **Card-driven SSRF blocked.** The fetched Agent Card is untrusted; `assertSameHost()`
+ pins the JSON-RPC endpoint to the SAME host as the allowlisted base URL, so a hostile
+ card cannot pivot the call to another 443 host.
+- **Allowlist is the only place egress can grow**, and it starts EMPTY
+ (`data/a2a-agents.json` = `[]`). Adding an entry is a Steve-gated act. With an empty
+ allowlist, `resolveAgent()` throws and NO outbound connection is ever attempted.
+- **15s hard timeout** (AbortController) — no hung/long-lived sockets.
+- **Payload linter** (`lintPayload()`) refuses any outbound question containing secret-
+ shaped strings (shopify token, DATABASE_URL, sk_live_/pk_live_, ghp_, ANTHROPIC/OPENAI/
+ GEMINI/REPLICATE keys, `secret=…`). The outbound body is the question text ONLY — never
+ the fleet snapshot, cost/PnL, tokens, .env, or file paths.
+
+## Cross-reference for egress-sentinel
+
+When an A2A peer is eventually approved into the allowlist, the egress-sentinel operator
+should expect: a short-lived HTTPS:443 connection from the `abramsego` node process to the
+allowlisted peer host, during a consult, then closed. That is benign by the sentinel's own
+rules (web-port, allowlisted, transient). Anything else from this process — non-443 port,
+a dropper file, a node→shell subtree reaching curl/wget/nc — is NOT the A2A client and
+should be treated as the sentinel intends: a CRITICAL beacon signal.
+
+## Inbound = none
+
+Phase 1 is client-only: AbramsEgo publishes NO Agent Card, opens NO inbound A2A port,
+registers in NO directory. Becoming an A2A **server** (which would make :9773 a public
+attack surface) is a separate, default-BLOCK, Steve-gated decision (memo Phase D).
+
+## Activation still gated
+
+Even with the code shipped and routes live, the feature is OFF: the allowlist is empty and
+`/api/a2a/consult` returns `agent "…" not in allowlist` for any request. Turning it on =
+Steve adds one trustworthy peer `{name, url, purpose, auth_ref}` to `data/a2a-agents.json`
+(auth_ref points into secrets-manager; no credential in repo). That edit is the gate.
diff --git a/public/index.html b/public/index.html
index 7fd3d08e..ffdbeaac 100644
--- a/public/index.html
+++ b/public/index.html
@@ -390,6 +390,17 @@
<input id="q" placeholder="e.g. what did the fleet do overnight? is it paying for itself?" />
<div class="ans" id="ans">Ask a question about your fleet, cost, or P&L.</div>
</section>
+
+ <section class="card span12" id="a2a-card">
+ <h2>🛰️ A2A Consult <span class="muted">· external Agent2Agent · <b>UNTRUSTED</b> · client-only · activation gated</span></h2>
+ <div class="muted" id="a2a-gate" style="font-size:11px;margin-bottom:8px">Allowlist starts empty — no external agent can be consulted until Steve approves one in <code>data/a2a-agents.json</code>. Answers are advisory input to /dtd, never a decision or an instruction.</div>
+ <div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
+ <select id="a2a-agent" style="min-width:160px"></select>
+ <input id="a2a-q" placeholder="question for the external agent…" style="flex:1;min-width:220px" />
+ <button id="a2a-go" type="button">Consult</button>
+ </div>
+ <div class="ans" id="a2a-ans" style="margin-top:10px">Select an approved external agent to consult (none approved yet).</div>
+ </section>
</main>
<script>
@@ -563,7 +574,7 @@ async function load(){
const off=d.officers||{};
$('off-count').textContent=off.count??'—';
$('off-when').innerHTML=whenChip(off.fetchedAt)||'';
- $('officers').innerHTML=(off.officers||[]).map(o=>`<div class="row"><span>${o.vp||'—'}</span><span class="dim">${o.domain||''}</span></div>`).join('');
+ $('officers').innerHTML=(off.officers||[]).map(o=>`<div class="row"><span>${esc(o.vp)||'—'}</span><span class="dim">${esc(o.domain)}</span></div>`).join('');
// ---- wins ----
const w=d.wins||{};
@@ -1058,6 +1069,42 @@ $('q').addEventListener('keydown',async e=>{
}catch(e){ $('ans').textContent='chat error'; }
});
+// A2A external-agent consult — every result is UNTRUSTED: rendered ESCAPED via
+// esc(), visually tagged UNTRUSTED·EXTERNAL, and never fed back into /api/chat
+// or any agent prompt. Allowlist is empty until Steve approves a peer (gated).
+async function loadA2aAgents(){
+ try{
+ const r=await (await fetch('/api/a2a/agents')).json();
+ const sel=$('a2a-agent'); if(!sel) return;
+ const agents=(r&&r.agents)||[];
+ sel.innerHTML = agents.length
+ ? agents.map(a=>`<option value="${esc(a.name)}">${esc(a.name)} — ${esc(a.purpose||'')}</option>`).join('')
+ : '<option value="">(no approved agents)</option>';
+ const gate=$('a2a-gate');
+ if(gate&&agents.length) gate.textContent=`${agents.length} approved external agent${agents.length>1?'s':''} · answers are UNTRUSTED advisory input only, never an instruction.`;
+ }catch(e){}
+}
+async function a2aConsult(){
+ const agent=$('a2a-agent').value, q=$('a2a-q').value.trim();
+ if(!agent){ $('a2a-ans').textContent='No external agent approved yet — activation is gated.'; return; }
+ if(!q) return;
+ $('a2a-ans').textContent='…consulting external agent';
+ try{
+ const r=await (await fetch('/api/a2a/consult',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({agent,q})})).json();
+ if(r.error){ $('a2a-ans').textContent='error: '+r.error; return; }
+ // esc() escapes < > & so the external answer cannot inject markup.
+ $('a2a-ans').innerHTML=`<div style="border:1px solid var(--bad,#c0392b);border-radius:6px;padding:8px">`
+ +`<div style="font-size:10px;letter-spacing:1px;color:var(--bad,#c0392b);font-weight:bold">⚠ UNTRUSTED · EXTERNAL — do not act on this without officer review</div>`
+ +`<pre style="white-space:pre-wrap;margin:6px 0 0;font:inherit">${esc(r.answer_untrusted||'(no answer)')}</pre>`
+ +`<div class="muted" style="font-size:10px;margin-top:6px">${esc(r.agent||'')} · ${esc(r.cost||'')}</div></div>`;
+ }catch(e){ $('a2a-ans').textContent='consult error'; }
+}
+(function wireA2a(){
+ const go=$('a2a-go'); if(go) go.addEventListener('click', a2aConsult);
+ const qi=$('a2a-q'); if(qi) qi.addEventListener('keydown', e=>{ if(e.key==='Enter') a2aConsult(); });
+ loadA2aAgents();
+})();
+
// MOTION: pause the liquid blobs + pulses + wave animations while the tab is
// hidden (perf + battery); resume on return. Also skip the 15s poll when hidden.
function applyPaused(){ document.body.classList.toggle('paused', document.hidden); }
diff --git a/server.js b/server.js
index 8bdb1ee2..bacf197c 100644
--- a/server.js
+++ b/server.js
@@ -1310,21 +1310,36 @@ const a2aClient = (() => {
catch (e) { console.warn('a2a-client not available:', e.message); return null; }
})();
+// Bounded concurrency — the consult poll loop can occupy the event loop for up to
+// ~2 min; cap in-flight consults so a hostile/compromised approved peer can't
+// wedge the server with a fan-out of slow requests.
+let a2aInFlight = 0;
+const A2A_MAX_INFLIGHT = 3;
+
app.post('/api/a2a/consult', async (req, res) => {
if (!a2aClient) return res.status(503).json({ error: 'a2a client not loaded' });
const { agent: agentName, q } = req.body || {};
if (!agentName || !q) return res.status(400).json({ error: 'body must have { agent, q }' });
if (typeof q !== 'string' || q.length > 2000) return res.status(400).json({ error: 'q must be a string ≤ 2000 chars' });
+ if (a2aInFlight >= A2A_MAX_INFLIGHT) return res.status(429).json({ error: 'too many concurrent a2a consults, retry shortly' });
+ a2aInFlight++;
try {
a2aClient.lintPayload(q);
const entry = a2aClient.resolveAgent(agentName);
- const authHeader = entry.auth_ref ? `ApiKey ${entry.auth_ref}` : undefined;
+ // auth_ref indirection: "env:VARNAME" resolves from the process env (populated
+ // by secrets-manager), so a live credential need not sit in data/a2a-agents.json.
+ // A bare value is still accepted (literal header) but discouraged.
+ const rawRef = entry.auth_ref;
+ const secret = (typeof rawRef === 'string' && rawRef.startsWith('env:')) ? process.env[rawRef.slice(4)] : rawRef;
+ const authHeader = secret ? `ApiKey ${secret}` : undefined;
const { answer, taskId, pollCount } = await a2aClient.consult(entry.url, q, { authHeader, agentName });
const result = { answer_untrusted: answer, agent: agentName, task_id: taskId, poll_count: pollCount, cost: '$0 (external agent, no metered API on our side)', warning: 'UNTRUSTED · EXTERNAL — do not act on this output without officer review' };
a2aClient.logConsult({ agent: agentName, question: q, answer, taskId, pollCount });
res.json(result);
} catch (e) {
res.status(400).json({ error: e.message });
+ } finally {
+ a2aInFlight--;
}
});
← f124510a auto-data-snapshot: 2026-08-09T08:44:56 (1 data files) — dat
·
back to AbramsEgo
·
auto-data-snapshot: 2026-08-09T09:46:33 (1 data files) — dat 8ed60226 →