@adapt-toolkit/a2adapt 0.11.4 → 0.11.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@
3
3
  "name": "a2adapt",
4
4
  "displayName": "a2adapt",
5
5
  "description": "Secure agent-to-agent communication channel over ADAPT: self-sovereign pubkey identity, end-to-end encryption, plan-first execution.",
6
- "version": "0.11.4",
6
+ "version": "0.11.5",
7
7
  "author": {
8
8
  "name": "Adapt Toolkit"
9
9
  },
package/dist/index.js CHANGED
@@ -22429,7 +22429,7 @@ var StreamableHTTPServerTransport = class {
22429
22429
  // src/index.ts
22430
22430
  import { resolve as resolve2, join as join2, dirname as dirname2, isAbsolute } from "node:path";
22431
22431
  import { fileURLToPath } from "node:url";
22432
- import { randomBytes, randomUUID } from "node:crypto";
22432
+ import { randomBytes, randomInt, randomUUID } from "node:crypto";
22433
22433
  import { createServer as createHttpServer } from "node:http";
22434
22434
  import * as fs2 from "node:fs";
22435
22435
  import { brotliCompressSync, brotliDecompressSync, constants as zlibConstants } from "node:zlib";
@@ -22512,7 +22512,7 @@ function writeIdentityFile(target, opts, overwrite = false) {
22512
22512
  }
22513
22513
 
22514
22514
  // src/index.ts
22515
- var VERSION = true ? "0.11.4" : "0.0.0-dev";
22515
+ var VERSION = true ? "0.11.5" : "0.0.0-dev";
22516
22516
  var CONFIG = loadConfig();
22517
22517
  var STATE_DIR = CONFIG.stateDir;
22518
22518
  var BROKER_URL = CONFIG.brokerUrl;
@@ -22668,7 +22668,8 @@ function describeIdentity(id) {
22668
22668
  bio: v.Reduce("bio").Visualize(),
22669
22669
  roleId: v.Reduce("role_id").Visualize(),
22670
22670
  rootCid: v.Reduce("root_cid").Visualize(),
22671
- rootName: v.Reduce("root_name").Visualize()
22671
+ rootName: v.Reduce("root_name").Visualize(),
22672
+ monitoringEnabled: v.Reduce("monitoring_enabled").GetBoolean()
22672
22673
  };
22673
22674
  }
22674
22675
  async function delegateRole(root, role) {
@@ -22735,6 +22736,282 @@ async function sendViaLocalBook(id, contact, text) {
22735
22736
  });
22736
22737
  return `"${entry.name}" was not a contact yet \u2014 connected via the local contact book and sent the message with the introduction. If "${entry.name}" requires approval for local introductions, delivery completes once they approve.`;
22737
22738
  }
22739
+ function monitoringStatus(id) {
22740
+ const v = readonlyTx(id, "::actor::get_monitoring_status");
22741
+ return {
22742
+ enabled: v.Reduce("monitoring_enabled").GetBoolean(),
22743
+ proxyCid: v.Reduce("proxy_cid").Visualize(),
22744
+ proxyPending: v.Reduce("proxy_pending").GetBoolean(),
22745
+ copiesQueued: parseInt(v.Reduce("copies_queued").Visualize(), 10) || 0,
22746
+ controlQueued: parseInt(v.Reduce("control_queued").Visualize(), 10) || 0
22747
+ };
22748
+ }
22749
+ async function setAgentMonitoring(root, role, enabled) {
22750
+ if (enabled) {
22751
+ const rootAd = exportAdBlob(root);
22752
+ await mutatingTx(role, "::actor::connect_sibling", {
22753
+ name: root.name,
22754
+ target_ad: role.pw.packet.NewBinaryFromBuffer(rootAd)
22755
+ });
22756
+ }
22757
+ const roleAd = exportAdBlob(role);
22758
+ const authData = await mutatingTx(root, "::actor::sign_monitoring_auth", {
22759
+ role_ad: root.pw.packet.NewBinaryFromBuffer(roleAd),
22760
+ enabled
22761
+ });
22762
+ const authBlob = Buffer.from(authData.Reduce("auth").GetBinary());
22763
+ await mutatingTx(role, "::actor::set_monitoring", {
22764
+ auth: role.pw.packet.NewBinaryFromBuffer(authBlob)
22765
+ });
22766
+ log(`[${role.name}] monitoring ${enabled ? "enabled" : "disabled"} (authorized by root "${root.name}")`);
22767
+ }
22768
+ async function sendControl(id, contactRef, payload) {
22769
+ await mutatingTx(id, "::a2a_control::send_control", {
22770
+ contact: contactRef,
22771
+ payload: JSON.stringify(payload)
22772
+ });
22773
+ }
22774
+ function renderCopies(v) {
22775
+ const out = [];
22776
+ if (v.IsNil()) return out;
22777
+ for (let i = 0; ; i++) {
22778
+ const m = v.Reduce(i);
22779
+ if (m.IsNil()) break;
22780
+ out.push({
22781
+ source_cid: m.Reduce("source_cid").Visualize(),
22782
+ source_name: m.Reduce("source_name").Visualize(),
22783
+ direction: m.Reduce("direction").Visualize(),
22784
+ peer_cid: m.Reduce("peer_cid").Visualize(),
22785
+ peer_name: m.Reduce("peer_name").Visualize(),
22786
+ date: m.Reduce("date").Visualize(),
22787
+ body: m.Reduce("body").Visualize()
22788
+ });
22789
+ }
22790
+ return out;
22791
+ }
22792
+ function renderControlRequests(v) {
22793
+ const out = [];
22794
+ if (v.IsNil()) return out;
22795
+ for (let i = 0; ; i++) {
22796
+ const m = v.Reduce(i);
22797
+ if (m.IsNil()) break;
22798
+ out.push({
22799
+ senderCid: m.Reduce("sender_cid").Visualize(),
22800
+ senderName: m.Reduce("sender_name").Visualize(),
22801
+ payload: m.Reduce("payload").Visualize(),
22802
+ date: m.Reduce("date").Visualize()
22803
+ });
22804
+ }
22805
+ return out;
22806
+ }
22807
+ var forwardBusy = /* @__PURE__ */ new Set();
22808
+ async function forwardMonitoring(root) {
22809
+ if (forwardBusy.has(root.name)) return;
22810
+ forwardBusy.add(root.name);
22811
+ try {
22812
+ const st = monitoringStatus(root);
22813
+ if (!st.proxyCid || st.copiesQueued === 0) return;
22814
+ const data = await mutatingTx(root, "::actor::get_monitoring_copies", {});
22815
+ const copies = renderCopies(data.Reduce("copies"));
22816
+ if (copies.length === 0) return;
22817
+ await sendControl(root, st.proxyCid, { v: 1, t: "monitoring", copies });
22818
+ } catch (err) {
22819
+ log(`[${root.name}] monitoring forward failed:`, String(err));
22820
+ } finally {
22821
+ forwardBusy.delete(root.name);
22822
+ }
22823
+ }
22824
+ function listAgentsFor(root) {
22825
+ const agents = [];
22826
+ for (const id of identities.values()) {
22827
+ if (id.name === root.name) continue;
22828
+ const info = describeIdentity(id);
22829
+ if (info.rootCid !== root.cid) continue;
22830
+ agents.push({
22831
+ name: id.name,
22832
+ cid: id.cid,
22833
+ role_id: info.roleId,
22834
+ bio: info.bio,
22835
+ monitoring: info.monitoringEnabled
22836
+ });
22837
+ }
22838
+ return agents;
22839
+ }
22840
+ function findAgentOf(root, ref) {
22841
+ const id = identities.get(ref) ?? [...identities.values()].find((i) => i.cid === ref);
22842
+ if (!id || id.name === root.name) return null;
22843
+ return describeIdentity(id).rootCid === root.cid ? id : null;
22844
+ }
22845
+ function deleteIdentityCompletely(id) {
22846
+ try {
22847
+ wrapper.remove_packet(id.cid);
22848
+ } catch (err) {
22849
+ log(`remove_packet(${id.cid}) failed:`, String(err));
22850
+ }
22851
+ identities.delete(id.name);
22852
+ try {
22853
+ unpublishFromBook(id.name);
22854
+ } catch (err) {
22855
+ log(`failed to unpublish "${id.name}" from the contact book:`, String(err));
22856
+ }
22857
+ const holder = bindingOwner.get(id.name);
22858
+ if (holder) {
22859
+ bindingOwner.delete(id.name);
22860
+ sessionBinding.delete(holder);
22861
+ persistBindings();
22862
+ }
22863
+ if (id.name === rootName) {
22864
+ rootName = null;
22865
+ clearRootMarker();
22866
+ }
22867
+ try {
22868
+ fs2.rmSync(id.dir, { recursive: true, force: true });
22869
+ } catch (err) {
22870
+ return `deleting ${id.dir} failed: ${String(err)}`;
22871
+ }
22872
+ return null;
22873
+ }
22874
+ async function handleControlRequest(root, req) {
22875
+ let msg;
22876
+ try {
22877
+ const parsed = JSON.parse(req.payload);
22878
+ if (!parsed || typeof parsed !== "object") throw new Error("not an object");
22879
+ msg = parsed;
22880
+ } catch {
22881
+ log(`[${root.name}] dropping unparseable control request from ${req.senderName}`);
22882
+ return;
22883
+ }
22884
+ if (msg.v !== 1 || typeof msg.t !== "string") {
22885
+ log(`[${root.name}] dropping control request with unknown envelope from ${req.senderName}`);
22886
+ return;
22887
+ }
22888
+ const reply = async (data) => {
22889
+ try {
22890
+ await sendControl(root, req.senderCid, { v: 1, t: "res", id: msg.id ?? null, req: msg.t, ...data });
22891
+ } catch (err) {
22892
+ log(`[${root.name}] control reply to ${req.senderName} failed:`, String(err));
22893
+ }
22894
+ };
22895
+ try {
22896
+ if (msg.t === "bind") {
22897
+ const data = await mutatingTx(root, "::actor::verify_proxy_code", {
22898
+ code: String(msg.code ?? ""),
22899
+ sender: req.senderCid
22900
+ });
22901
+ if (!data.Reduce("verified").GetBoolean()) {
22902
+ const reason = data.Reduce("reason").Visualize();
22903
+ log(`[${root.name}] proxy bind attempt from ${req.senderName} rejected (${reason})`);
22904
+ await reply({ ok: false, error: reason });
22905
+ return;
22906
+ }
22907
+ appendNotifyLog(root, { event: "monitoring_proxy_bound", from: req.senderName });
22908
+ log(`[${root.name}] monitoring proxy bound: ${req.senderName} (${req.senderCid})`);
22909
+ await reply({ ok: true, root: { name: root.name, cid: root.cid }, agents: listAgentsFor(root) });
22910
+ await forwardMonitoring(root);
22911
+ return;
22912
+ }
22913
+ const st = monitoringStatus(root);
22914
+ if (!st.proxyCid || st.proxyCid !== req.senderCid) {
22915
+ await reply({ ok: false, error: "not_authorized" });
22916
+ return;
22917
+ }
22918
+ switch (msg.t) {
22919
+ case "list_agents": {
22920
+ await reply({ ok: true, root: { name: root.name, cid: root.cid }, agents: listAgentsFor(root) });
22921
+ return;
22922
+ }
22923
+ case "create_agent": {
22924
+ const name = String(msg.name ?? "").trim();
22925
+ const bio = String(msg.bio ?? "");
22926
+ const bad = validateName(name);
22927
+ if (bad) {
22928
+ await reply({ ok: false, error: bad });
22929
+ return;
22930
+ }
22931
+ if (identities.has(name)) {
22932
+ await reply({ ok: false, error: `an identity named "${name}" already exists` });
22933
+ return;
22934
+ }
22935
+ const agent = await provisionIdentity(name);
22936
+ if (bio) await mutatingTx(agent, "::a2a_messaging::set_my_bio", { bio });
22937
+ await delegateRole(root, agent);
22938
+ await reply({ ok: true, agents: listAgentsFor(root) });
22939
+ return;
22940
+ }
22941
+ case "update_role": {
22942
+ const agent = findAgentOf(root, String(msg.agent ?? ""));
22943
+ if (!agent) {
22944
+ await reply({ ok: false, error: "no such agent under this root" });
22945
+ return;
22946
+ }
22947
+ await mutatingTx(agent, "::a2a_messaging::set_my_bio", { bio: String(msg.bio ?? "") });
22948
+ await reply({ ok: true, agents: listAgentsFor(root) });
22949
+ return;
22950
+ }
22951
+ case "set_monitoring": {
22952
+ const agent = findAgentOf(root, String(msg.agent ?? ""));
22953
+ if (!agent) {
22954
+ await reply({ ok: false, error: "no such agent under this root" });
22955
+ return;
22956
+ }
22957
+ await setAgentMonitoring(root, agent, Boolean(msg.enabled));
22958
+ await reply({ ok: true, agents: listAgentsFor(root) });
22959
+ return;
22960
+ }
22961
+ case "contact_agent": {
22962
+ const agent = findAgentOf(root, String(msg.agent ?? ""));
22963
+ if (!agent) {
22964
+ await reply({ ok: false, error: "no such agent under this root" });
22965
+ return;
22966
+ }
22967
+ const inv = await mutatingTx(agent, "::a2a_messaging::generate_invite", {});
22968
+ const blob = packInvite(Buffer.from(inv.Reduce("invite").GetBinary()));
22969
+ await reply({ ok: true, agent: agent.name, invite: blob });
22970
+ return;
22971
+ }
22972
+ case "remove_agent": {
22973
+ const agent = findAgentOf(root, String(msg.agent ?? ""));
22974
+ if (!agent) {
22975
+ await reply({ ok: false, error: "no such agent under this root" });
22976
+ return;
22977
+ }
22978
+ const fail = deleteIdentityCompletely(agent);
22979
+ if (fail) {
22980
+ await reply({ ok: false, error: fail });
22981
+ return;
22982
+ }
22983
+ await reply({ ok: true, agents: listAgentsFor(root) });
22984
+ return;
22985
+ }
22986
+ default:
22987
+ await reply({ ok: false, error: `unknown request type "${msg.t}"` });
22988
+ }
22989
+ } catch (err) {
22990
+ await reply({ ok: false, error: String(err) });
22991
+ }
22992
+ }
22993
+ var controlBusy = /* @__PURE__ */ new Set();
22994
+ async function processControlRequests(root) {
22995
+ if (controlBusy.has(root.name)) return;
22996
+ controlBusy.add(root.name);
22997
+ try {
22998
+ for (; ; ) {
22999
+ const data = await mutatingTx(root, "::actor::get_control_requests", {});
23000
+ const reqs = renderControlRequests(data.Reduce("requests"));
23001
+ if (reqs.length === 0) return;
23002
+ for (const req of reqs) {
23003
+ await handleControlRequest(root, req);
23004
+ }
23005
+ }
23006
+ } catch (err) {
23007
+ log(`[${root.name}] control dispatch failed:`, String(err));
23008
+ } finally {
23009
+ controlBusy.delete(root.name);
23010
+ }
23011
+ if (identities.has(root.name) && monitoringStatus(root).controlQueued > 0) {
23012
+ return processControlRequests(root);
23013
+ }
23014
+ }
22738
23015
  async function ensureRegistrar() {
22739
23016
  fs2.mkdirSync(bookDir(), { recursive: true });
22740
23017
  let seed;
@@ -22897,6 +23174,12 @@ function wireHandlers(id) {
22897
23174
  process.nextTick(
22898
23175
  () => pushNotification(id.name, `[${id.name}] "${name}" queued a message awaiting introduction approval (${queued} queued).`)
22899
23176
  );
23177
+ } else if (event === "control_request") {
23178
+ const from = payload.Reduce("sender_name").Visualize();
23179
+ log(`[${id.name}] control request queued by ${from}`);
23180
+ process.nextTick(() => void processControlRequests(id));
23181
+ } else if (event === "monitoring_copy") {
23182
+ process.nextTick(() => void forwardMonitoring(id));
22900
23183
  }
22901
23184
  return;
22902
23185
  }
@@ -23034,6 +23317,16 @@ async function bootWrapper() {
23034
23317
  clearRootMarker();
23035
23318
  }
23036
23319
  if (rootName) log(`root identity: ${rootName}`);
23320
+ const root = rootName ? identities.get(rootName) : void 0;
23321
+ if (root) {
23322
+ try {
23323
+ const st = monitoringStatus(root);
23324
+ if (st.controlQueued > 0) void processControlRequests(root);
23325
+ else if (st.copiesQueued > 0) void forwardMonitoring(root);
23326
+ } catch (err) {
23327
+ log(`boot-time monitoring/control drain failed:`, String(err));
23328
+ }
23329
+ }
23037
23330
  persistBindings();
23038
23331
  }
23039
23332
  function resolveBound(sessionId) {
@@ -23391,31 +23684,9 @@ Bio: ${info.bio}` : "";
23391
23684
  );
23392
23685
  }
23393
23686
  }
23394
- try {
23395
- wrapper.remove_packet(id.cid);
23396
- } catch (err) {
23397
- log(`remove_packet(${id.cid}) failed:`, String(err));
23398
- }
23399
- identities.delete(name);
23400
- try {
23401
- unpublishFromBook(name);
23402
- } catch (err) {
23403
- log(`failed to unpublish "${name}" from the contact book:`, String(err));
23404
- }
23405
- const holder = bindingOwner.get(name);
23406
- if (holder) {
23407
- bindingOwner.delete(name);
23408
- sessionBinding.delete(holder);
23409
- persistBindings();
23410
- }
23411
- if (name === rootName) {
23412
- rootName = null;
23413
- clearRootMarker();
23414
- }
23415
- try {
23416
- fs2.rmSync(id.dir, { recursive: true, force: true });
23417
- } catch (err) {
23418
- return textResult(`Identity "${name}" removed from memory, but deleting ${id.dir} failed: ${String(err)}`, true);
23687
+ const fail = deleteIdentityCompletely(id);
23688
+ if (fail) {
23689
+ return textResult(`Identity "${name}" removed from memory, but ${fail}`, true);
23419
23690
  }
23420
23691
  return textResult(`Removed identity "${name}" and its state.`);
23421
23692
  }
@@ -23731,6 +24002,97 @@ These are now marked processed (auto-GC'd later). To hand any back to another se
23731
24002
  }
23732
24003
  }
23733
24004
  );
24005
+ const rootOr = () => {
24006
+ const root = rootName ? identities.get(rootName) : void 0;
24007
+ if (!root) {
24008
+ return { err: textResult("No root identity exists on this host \u2014 create one with create_root_identity first.", true) };
24009
+ }
24010
+ return { root };
24011
+ };
24012
+ server.tool(
24013
+ "enable_monitoring",
24014
+ "Enable monitoring on one of this host's agents (a role under the root identity): from now on the agent reports a copy of every message it sends or receives to the root, which forwards them to the bound browser proxy (see bind_monitoring_proxy). Authorized by a root-signed blob the role verifies against its delegation chain. Forward-only: past messages are never reported.",
24015
+ { agent: external_exports.string().min(1).describe("Agent (role) name or container id.") },
24016
+ async ({ agent }) => {
24017
+ const { root, err } = rootOr();
24018
+ if (err) return err;
24019
+ const role = findAgentOf(root, agent);
24020
+ if (!role) return textResult(`enable_monitoring failed: "${agent}" is not a role under root "${root.name}".`, true);
24021
+ try {
24022
+ await setAgentMonitoring(root, role, true);
24023
+ return textResult(`Monitoring enabled on "${role.name}" \u2014 its message traffic now reports to root "${root.name}".`);
24024
+ } catch (e) {
24025
+ return textResult(`enable_monitoring failed: ${String(e)}`, true);
24026
+ }
24027
+ }
24028
+ );
24029
+ server.tool(
24030
+ "disable_monitoring",
24031
+ "Disable monitoring on one of this host's agents (see enable_monitoring).",
24032
+ { agent: external_exports.string().min(1).describe("Agent (role) name or container id.") },
24033
+ async ({ agent }) => {
24034
+ const { root, err } = rootOr();
24035
+ if (err) return err;
24036
+ const role = findAgentOf(root, agent);
24037
+ if (!role) return textResult(`disable_monitoring failed: "${agent}" is not a role under root "${root.name}".`, true);
24038
+ try {
24039
+ await setAgentMonitoring(root, role, false);
24040
+ return textResult(`Monitoring disabled on "${role.name}".`);
24041
+ } catch (e) {
24042
+ return textResult(`disable_monitoring failed: ${String(e)}`, true);
24043
+ }
24044
+ }
24045
+ );
24046
+ server.tool(
24047
+ "bind_monitoring_proxy",
24048
+ "Start binding a browser (messenger) account as this host's monitoring & control proxy. PREREQUISITE: the browser account must already be a contact of the ROOT identity (invite exchange). This generates a 6-digit code (5-minute expiry, 3 attempts) bound to that contact and shows it HERE \u2014 read it to the user, who enters it in the messenger's Control Panel. On a successful code verification the contact becomes the monitoring proxy: it receives the monitoring feed and may manage agents (create, edit bios, toggle monitoring, request invites) through the root.",
24049
+ { contact: external_exports.string().min(1).describe("The root's contact (name or container id) to bind as the proxy.") },
24050
+ async ({ contact }) => {
24051
+ const { root, err } = rootOr();
24052
+ if (err) return err;
24053
+ try {
24054
+ const code = String(randomInt(0, 1e6)).padStart(6, "0");
24055
+ const data = await mutatingTx(root, "::actor::set_proxy_pending", { code, proxy: contact });
24056
+ const cid = data.Reduce("proxy_cid").Visualize();
24057
+ return textResult(
24058
+ `Proxy binding started for contact "${contact}" (${cid}).
24059
+
24060
+ Verification code: ${code}
24061
+
24062
+ Tell the user to enter this code in the messenger's Control Panel within 5 minutes (3 attempts). Do NOT send the code over a2adapt \u2014 it must travel out-of-band (this terminal counts).`
24063
+ );
24064
+ } catch (e) {
24065
+ return textResult(`bind_monitoring_proxy failed: ${String(e)}`, true);
24066
+ }
24067
+ }
24068
+ );
24069
+ server.tool(
24070
+ "get_monitoring_status",
24071
+ "Report the monitoring & control state of this host: the root's bound proxy (if any), a pending proxy verification, queued copies/requests, and which agents have monitoring enabled.",
24072
+ {},
24073
+ async () => {
24074
+ const { root, err } = rootOr();
24075
+ if (err) return err;
24076
+ try {
24077
+ const st = monitoringStatus(root);
24078
+ const lines = [];
24079
+ lines.push(`Root "${root.name}" (${root.cid}):`);
24080
+ lines.push(st.proxyCid ? `\u2022 monitoring proxy bound: ${st.proxyCid}` : "\u2022 no monitoring proxy bound");
24081
+ if (st.proxyPending) lines.push("\u2022 a proxy code verification is pending");
24082
+ if (st.copiesQueued > 0) lines.push(`\u2022 ${st.copiesQueued} monitoring cop${st.copiesQueued === 1 ? "y" : "ies"} queued for forwarding`);
24083
+ if (st.controlQueued > 0) lines.push(`\u2022 ${st.controlQueued} control request(s) queued`);
24084
+ const agents = listAgentsFor(root);
24085
+ lines.push("");
24086
+ lines.push(
24087
+ agents.length === 0 ? "No agents (roles) under this root." : `Agents (${agents.length}):
24088
+ ${agents.map((a) => `\u2022 ${a.name} \u2014 monitoring ${a.monitoring ? "ON" : "off"}${a.bio ? ` \u2014 ${a.bio}` : ""}`).join("\n")}`
24089
+ );
24090
+ return textResult(lines.join("\n"));
24091
+ } catch (e) {
24092
+ return textResult(`get_monitoring_status failed: ${String(e)}`, true);
24093
+ }
24094
+ }
24095
+ );
23734
24096
  return server;
23735
24097
  }
23736
24098
  function readBody(req) {
@@ -62,9 +62,23 @@
62
62
  // reject_introduction — drop a pending local introduction
63
63
  // list_pending_introductions — (readonly) pending introductions (names + queue sizes)
64
64
  //
65
+ // Monitoring + control plane (host-fired unless noted; see
66
+ // MONITORING-AND-SHARED-LIBRARY-DESIGN.md):
67
+ // get_monitoring_status — (readonly) enabled flag / proxy binding / queue sizes
68
+ // sign_monitoring_auth — root-only: sign an enable/disable auth for a role
69
+ // set_monitoring — role: verify the root-signed auth, set the flag
70
+ // get_monitoring_copies — root: drain queued copies for proxy forwarding
71
+ // get_control_requests — root: drain queued proxy control requests
72
+ // set_proxy_pending — root: store the host-generated 6-digit code
73
+ // verify_proxy_code — root: check a bind attempt (atomic attempts/expiry)
74
+ // clear_monitoring_proxy — root: drop the proxy binding
75
+ // ::a2a_control::send_control — send an opaque control payload to a contact
76
+ //
65
77
  // External transactions (inbound, not exposed as tools):
66
78
  // accept_contact — inviter learns the joiner's identity + name (core shim)
67
79
  // receive_message — store a decrypted inbound message (core shim)
80
+ // receive_monitoring_copy — a monitored role of mine reports a message copy
81
+ // ::a2a_control::control_message — control payload from a contact (queued for the daemon)
68
82
  // local_introduce — same-host peer connects via the local contact book
69
83
  // sibling_introduce — intra-root peer connects, authorized by its delegation cert
70
84
 
@@ -82,6 +96,7 @@ application actor loads libraries
82
96
  current_transaction_info,
83
97
  a2a_protocol,
84
98
  a2a_messaging,
99
+ a2a_control,
85
100
  version
86
101
  uses transactions
87
102
  {
@@ -113,6 +128,35 @@ application actor loads libraries
113
128
  seen_nonce_cap is int = 1024.
114
129
  pending_queue_cap is int = 50.
115
130
 
131
+ // ---- monitoring shapes + limits (see MONITORING-AND-SHARED-LIBRARY-DESIGN.md) ----
132
+ // Root-signed authorization for flipping a role's monitoring flag. Like
133
+ // a delegation cert, verified against the role's pinned root keys, so
134
+ // only this hierarchy's root can change what its roles report.
135
+ metadef monitoring_auth_core_t: ($version -> int, $role_cid -> global_id, $enabled -> bool, $issued_at -> time).
136
+ metadef monitoring_auth_t: ($c -> monitoring_auth_core_t, $s -> crypto_signature).
137
+ // One monitored message, as the role reports it to its root.
138
+ metadef monitoring_copy_t: (
139
+ $version -> int,
140
+ $source_cid -> global_id,
141
+ $source_name -> str,
142
+ $direction -> str,
143
+ $peer_cid -> global_id,
144
+ $peer_name -> str,
145
+ $date -> time,
146
+ $body -> str
147
+ ).
148
+ // Proxy binding (root only): a pending 6-digit-code verification and
149
+ // the verified binding that replaces it on success.
150
+ metadef proxy_pending_t: ($code -> str, $proxy_cid -> global_id, $created_at -> time, $attempts -> int).
151
+ metadef proxy_binding_t: ($proxy_cid -> global_id, $bound_at -> time).
152
+ // One queued control request from the bound browser proxy.
153
+ metadef control_req_t: ($sender_cid -> global_id, $sender_name -> str, $payload -> str, $date -> time).
154
+
155
+ proxy_code_max_age_seconds is int = 300.
156
+ proxy_max_attempts is int = 3.
157
+ monitoring_inbox_cap is int = 500.
158
+ control_inbox_cap is int = 200.
159
+
116
160
  // Wire the deserialization primitive into the libraries that need it.
117
161
  _read_or_abort = grab( _read_or_abort ).
118
162
  key_storage::init ($_read_or_abort -> _read_or_abort).
@@ -147,6 +191,23 @@ application actor loads libraries
147
191
  // each with a bounded queue of messages awaiting approval.
148
192
  pending_introductions is (global_id ->> pending_intro_t) = (,).
149
193
 
194
+ // ---- monitoring state -------------------------------------------------
195
+ // Whether THIS packet (a role) reports its message traffic to its root.
196
+ // Only flipped by a root-signed authorization (set_monitoring).
197
+ monitoring_enabled is bool = FALSE.
198
+ // Root only: copies received from monitored roles, awaiting the host's
199
+ // get_monitoring_copies pull (forwarded to the bound proxy). Capped;
200
+ // oldest copies are dropped first when no proxy drains the queue.
201
+ monitoring_inbox is monitoring_copy_t[] = [].
202
+ // Root only: the in-flight proxy binding (6-digit code verification)
203
+ // and the verified proxy that monitoring traffic is forwarded to.
204
+ proxy_pending is proxy_pending_t+ = NIL.
205
+ monitoring_proxy is proxy_binding_t+ = NIL.
206
+ // Root only: control requests from the bound proxy, awaiting the
207
+ // host's get_control_requests pull. Kept out of the message inbox so
208
+ // agent sessions never see them.
209
+ control_inbox is control_req_t[] = [].
210
+
150
211
  // Signal the host to persist the packet. Only emitted at the end of a
151
212
  // complete procedure — intermediate states (e.g. channel handshake) are
152
213
  // never saved, so a crash mid-handshake restores to the last stable point.
@@ -170,6 +231,39 @@ application actor loads libraries
170
231
  return mid.
171
232
  }
172
233
 
234
+ // Build the monitoring-copy action for one message IF this packet is a
235
+ // monitored role with a live encrypted channel to its root; [] otherwise.
236
+ // The is_container_registered guard makes a missing/lost root channel
237
+ // degrade to "no copy" instead of failing the user's message — the
238
+ // enable flow (host-side) establishes the channel via connect_sibling.
239
+ fn monitor_copy_actions (direction: str, peer_cid: global_id, text: str, msg_date: time) -> transaction::action::type[]
240
+ {
241
+ if monitoring_enabled == FALSE || a2a_messaging::delegation_cert == NIL { return []. }
242
+ root_cid = a2a_messaging::delegation_cert? $c $root_cid.
243
+ if key_storage::is_container_registered(root_cid) != TRUE { return []. }
244
+
245
+ peer_name is str = "".
246
+ p = a2a_messaging::contacts peer_cid.
247
+ if p != NIL { peer_name -> p? $name. }
248
+
249
+ copy is monitoring_copy_t = (
250
+ $version -> 1,
251
+ $source_cid -> _get_container_id(),
252
+ $source_name -> a2a_messaging::my_name,
253
+ $direction -> direction,
254
+ $peer_cid -> peer_cid,
255
+ $peer_name -> peer_name,
256
+ $date -> msg_date,
257
+ $body -> text
258
+ ).
259
+ return [
260
+ encrypted_channel::send_encrypted_tx root_cid (
261
+ $name -> "::actor::receive_monitoring_copy",
262
+ $targ -> ($copy -> copy)
263
+ )
264
+ ].
265
+ }
266
+
173
267
  // Resolve a pending introduction by joiner name or stringified container
174
268
  // id; aborts when nothing matches.
175
269
  fn resolve_pending (ref: str) -> global_id
@@ -217,13 +311,44 @@ application actor loads libraries
217
311
 
218
312
  sender_name = (arg $sender_name) safe str.
219
313
  mid = deposit_message sender_id sender_name text msg_date.
314
+ actions is transaction::action::type[] = [].
315
+ sc monitor_copy_actions "in" sender_id text msg_date -- ( -> a)
316
+ {
317
+ actions (_count actions|) -> a.
318
+ }
319
+ actions (_count actions|) -> _notify_agent ($event -> $message_received, $sender_name -> sender_name, $msg_id -> mid, $date -> msg_date).
320
+ actions (_count actions|) -> _save_state NIL.
321
+ return actions.
322
+ },
323
+ $on_message_sent -> fn (arg: any) -> transaction::action::type[]
324
+ {
325
+ return monitor_copy_actions "out" ((arg $target_id) safe global_id) ((arg $text) safe str) ((arg $date) safe time).
326
+ },
327
+ $on_contact_removed -> fn (_: any) -> transaction::action::type[] { return []. }
328
+ ).
329
+
330
+ // Wire the control plane (see MONITORING-AND-SHARED-LIBRARY-DESIGN.md
331
+ // Part 4): control requests from the bound browser proxy queue in
332
+ // control_inbox — NEVER the message inbox, so agent sessions don't see
333
+ // them — and the notify event wakes the daemon's dispatcher. The
334
+ // payload stays opaque here; sender authorization happens in the
335
+ // daemon against the packet's monitoring_proxy / proxy_pending state.
336
+ a2a_control::init (
337
+ $on_control_received -> fn (arg: any) -> transaction::action::type[]
338
+ {
339
+ abort "Control queue is full." when (_count control_inbox|) >= control_inbox_cap.
340
+ sender_name = (arg $sender_name) safe str.
341
+ control_inbox (_count control_inbox|) -> (
342
+ $sender_cid -> (arg $sender_id) safe global_id,
343
+ $sender_name -> sender_name,
344
+ $payload -> (arg $payload) safe str,
345
+ $date -> (arg $date) safe time
346
+ ).
220
347
  return [
221
- _notify_agent ($event -> $message_received, $sender_name -> sender_name, $msg_id -> mid, $date -> msg_date),
348
+ _notify_agent ($event -> $control_request, $sender_name -> sender_name, $queued -> _count control_inbox|),
222
349
  _save_state NIL
223
350
  ].
224
- },
225
- $on_message_sent -> fn (_: any) -> transaction::action::type[] { return []. },
226
- $on_contact_removed -> fn (_: any) -> transaction::action::type[] { return []. }
351
+ }
227
352
  ).
228
353
  }
229
354
 
@@ -633,7 +758,7 @@ application actor loads libraries
633
758
  {
634
759
  if a2a_messaging::delegation_cert == NIL
635
760
  {
636
- return ($name -> a2a_messaging::my_name, $bio -> a2a_messaging::my_bio, $has_cert -> FALSE, $role_id -> "", $root_cid -> "", $root_name -> "").
761
+ return ($name -> a2a_messaging::my_name, $bio -> a2a_messaging::my_bio, $has_cert -> FALSE, $role_id -> "", $root_cid -> "", $root_name -> "", $monitoring_enabled -> monitoring_enabled).
637
762
  }
638
763
  cert = a2a_messaging::delegation_cert?.
639
764
  rname is str = "".
@@ -644,7 +769,8 @@ application actor loads libraries
644
769
  $has_cert -> TRUE,
645
770
  $role_id -> cert $c $role_id,
646
771
  $root_cid -> (_str (cert $c $root_cid)),
647
- $root_name -> rname
772
+ $root_name -> rname,
773
+ $monitoring_enabled -> monitoring_enabled
648
774
  ).
649
775
  }
650
776
 
@@ -692,6 +818,232 @@ application actor loads libraries
692
818
  }).
693
819
  }
694
820
 
821
+ // ---- monitoring + control plane -------------------------------------------
822
+ // (see MONITORING-AND-SHARED-LIBRARY-DESIGN.md). A monitored ROLE reports
823
+ // every message it sends/receives to its ROOT (the monitor_copy_actions
824
+ // branches in the storage hooks above); the root queues the copies and the
825
+ // host forwards them to a human proxy bound via 6-digit-code verification.
826
+ // The proxy's control requests (create agent, update role, …) queue in
827
+ // control_inbox and are executed by the host daemon.
828
+
829
+ trn readonly get_monitoring_status _
830
+ {
831
+ pending is bool = FALSE.
832
+ if proxy_pending != NIL { pending -> TRUE. }
833
+ proxy_out is str = "".
834
+ if monitoring_proxy != NIL { proxy_out -> _str (monitoring_proxy? $proxy_cid). }
835
+ return (
836
+ $monitoring_enabled -> monitoring_enabled,
837
+ $proxy_cid -> proxy_out,
838
+ $proxy_pending -> pending,
839
+ $copies_queued -> _count monitoring_inbox|,
840
+ $control_queued -> _count control_inbox|
841
+ ).
842
+ }
843
+
844
+ // Sign a monitoring authorization for a role (ROOT packet only — the role
845
+ // verifies the signature against its pinned root keys, so an auth minted
846
+ // by any other packet fails). Stateless, mirrors sign_delegation.
847
+ trn sign_monitoring_auth _:($role_ad -> role_ad_blob: bin, $enabled -> enabled: bool)
848
+ {
849
+ current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
850
+ abort "Only a root identity can sign monitoring authorizations." when a2a_messaging::delegation_cert != NIL.
851
+
852
+ role_ad = (_read_or_abort role_ad_blob) safe address_document_types::t_address_document.
853
+ core is monitoring_auth_core_t = (
854
+ $version -> 1,
855
+ $role_cid -> role_ad $identity $container_id,
856
+ $enabled -> enabled,
857
+ $issued_at -> (current_transaction_info::get_transaction_time())?
858
+ ).
859
+ auth is monitoring_auth_t = ($c -> core, $s -> key_storage::default_sign (_value_id core)).
860
+ return transaction::success [
861
+ _return_data ($auth -> (_write auth))
862
+ ].
863
+ }
864
+
865
+ // Store a verified monitoring flag (ROLE packet, host-fired after the root
866
+ // signed the auth). An auth that does not name me or was not signed by my
867
+ // root is rejected — so even a compromised host process cannot silently
868
+ // flip monitoring without the root packet's keys.
869
+ trn set_monitoring _:($auth -> auth_blob: bin)
870
+ {
871
+ current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
872
+ abort "Only a delegated role can be monitored." when a2a_messaging::delegation_cert == NIL || a2a_messaging::root_ad == NIL.
873
+
874
+ auth = (_read_or_abort auth_blob) safe monitoring_auth_t.
875
+ abort "Unsupported monitoring authorization version." when (auth $c $version) != 1.
876
+ abort "This monitoring authorization was issued to a different identity." when (auth $c $role_cid) != _get_container_id().
877
+ abort "The monitoring authorization was not signed by my root." when key_storage::check_signature_new_container (_value_id (auth $c)) (auth $s) (a2a_messaging::root_ad? $identity $key_list) != TRUE.
878
+
879
+ monitoring_enabled -> auth $c $enabled.
880
+ return transaction::success [
881
+ _return_data ($monitoring_enabled -> monitoring_enabled),
882
+ _save_state NIL
883
+ ].
884
+ }
885
+
886
+ // A monitored role reports one message copy (ROOT packet, inbound). Only
887
+ // accepted from a verified role of THIS root (the contact_roots linkage
888
+ // recorded by sibling_introduce), and the copy must name its actual sender
889
+ // — a role cannot forge copies on another role's behalf.
890
+ trn receive_monitoring_copy _:($copy -> copy: monitoring_copy_t)
891
+ {
892
+ current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::external,).
893
+ encrypted_channel::check_encrypted_or_abort().
894
+
895
+ sender_id = current_transaction_info::get_external_envelope_or_abort() $from.
896
+ link = a2a_messaging::contact_roots sender_id.
897
+ abort "Monitoring copies are only accepted from my own roles." when link == NIL || (link? $root_cid) != _get_container_id().
898
+ abort "Monitoring copy does not name its sender as the source." when (copy $source_cid) != sender_id.
899
+ abort "Unsupported monitoring copy version." when (copy $version) != 1.
900
+
901
+ // Capped queue, oldest first out: if no proxy drains the root, recent
902
+ // traffic wins over old.
903
+ if (_count monitoring_inbox|) >= monitoring_inbox_cap
904
+ {
905
+ trimmed is monitoring_copy_t[] = [].
906
+ i is int = 0.
907
+ sc monitoring_inbox -- ( -> m)
908
+ {
909
+ if i > 0 { trimmed (_count trimmed|) -> m. }
910
+ i -> i + 1.
911
+ }
912
+ monitoring_inbox -> trimmed.
913
+ }
914
+ monitoring_inbox (_count monitoring_inbox|) -> copy.
915
+
916
+ return transaction::success [
917
+ _notify_agent ($event -> $monitoring_copy, $source_name -> copy $source_name, $queued -> _count monitoring_inbox|),
918
+ _save_state NIL
919
+ ].
920
+ }
921
+
922
+ // Drain the queued monitoring copies (ROOT packet, host-fired before
923
+ // forwarding to the bound proxy). Cleared on read, like get_messages.
924
+ trn get_monitoring_copies _
925
+ {
926
+ current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
927
+
928
+ copies = monitoring_inbox.
929
+ monitoring_inbox -> [].
930
+ return transaction::success [
931
+ _return_data ($copies -> copies),
932
+ _save_state NIL
933
+ ].
934
+ }
935
+
936
+ // Drain the queued control requests (ROOT packet, host-fired by the
937
+ // control dispatcher). Cleared on read.
938
+ trn get_control_requests _
939
+ {
940
+ current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
941
+
942
+ reqs = control_inbox.
943
+ control_inbox -> [].
944
+ return transaction::success [
945
+ _return_data ($requests -> reqs),
946
+ _save_state NIL
947
+ ].
948
+ }
949
+
950
+ // Start a proxy binding (ROOT packet, host-fired): remember the code the
951
+ // host generated (MUFL has no random source) for one specific contact.
952
+ // Restarting overwrites any previous pending binding.
953
+ trn set_proxy_pending _:($code -> code: str, $proxy -> proxy_ref: str)
954
+ {
955
+ current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
956
+ abort "Only a root identity can bind a monitoring proxy." when a2a_messaging::delegation_cert != NIL.
957
+
958
+ pid = a2a_messaging::resolve_contact proxy_ref.
959
+ proxy_pending -> (
960
+ $code -> code,
961
+ $proxy_cid -> pid,
962
+ $created_at -> (current_transaction_info::get_transaction_time())?,
963
+ $attempts -> 0
964
+ ).
965
+ return transaction::success [
966
+ _return_data ($pending -> TRUE, $proxy_cid -> (_str pid)),
967
+ _save_state NIL
968
+ ].
969
+ }
970
+
971
+ // Verify a proxy's code attempt (ROOT packet, host-fired when a `bind`
972
+ // control request arrives). Failures are returned as DATA — not aborts —
973
+ // so the attempt counter and expiry clearing persist atomically; an abort
974
+ // would roll them back and reopen the brute-force window.
975
+ trn verify_proxy_code _:($code -> code: str, $sender -> sender_ref: str)
976
+ {
977
+ current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
978
+
979
+ if proxy_pending == NIL
980
+ {
981
+ return transaction::success [ _return_data ($verified -> FALSE, $reason -> "no_pending") ].
982
+ }
983
+ p = proxy_pending?.
984
+ now = (current_transaction_info::get_transaction_time())?.
985
+
986
+ if (_substract_seconds now (p $created_at)) > proxy_code_max_age_seconds
987
+ {
988
+ proxy_pending -> NIL.
989
+ return transaction::success [
990
+ _return_data ($verified -> FALSE, $reason -> "expired"),
991
+ _save_state NIL
992
+ ].
993
+ }
994
+
995
+ sid = a2a_messaging::resolve_contact sender_ref.
996
+ if sid != (p $proxy_cid)
997
+ {
998
+ // Not the contact this binding was started for: reject without
999
+ // burning an attempt (the code was never compared).
1000
+ return transaction::success [ _return_data ($verified -> FALSE, $reason -> "wrong_sender") ].
1001
+ }
1002
+
1003
+ if code != (p $code)
1004
+ {
1005
+ attempts = (p $attempts) + 1.
1006
+ if attempts >= proxy_max_attempts
1007
+ {
1008
+ proxy_pending -> NIL.
1009
+ return transaction::success [
1010
+ _return_data ($verified -> FALSE, $reason -> "too_many_attempts"),
1011
+ _save_state NIL
1012
+ ].
1013
+ }
1014
+ proxy_pending -> (
1015
+ $code -> p $code,
1016
+ $proxy_cid -> p $proxy_cid,
1017
+ $created_at -> p $created_at,
1018
+ $attempts -> attempts
1019
+ ).
1020
+ return transaction::success [
1021
+ _return_data ($verified -> FALSE, $reason -> "wrong_code", $attempts_left -> proxy_max_attempts - attempts),
1022
+ _save_state NIL
1023
+ ].
1024
+ }
1025
+
1026
+ monitoring_proxy -> ($proxy_cid -> sid, $bound_at -> now).
1027
+ proxy_pending -> NIL.
1028
+ return transaction::success [
1029
+ _return_data ($verified -> TRUE, $proxy_cid -> (_str sid)),
1030
+ _save_state NIL
1031
+ ].
1032
+ }
1033
+
1034
+ // Drop the proxy binding (and any in-flight code verification).
1035
+ trn clear_monitoring_proxy _
1036
+ {
1037
+ current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
1038
+
1039
+ monitoring_proxy -> NIL.
1040
+ proxy_pending -> NIL.
1041
+ return transaction::success [
1042
+ _return_data ($cleared -> TRUE),
1043
+ _save_state NIL
1044
+ ].
1045
+ }
1046
+
695
1047
  // ---- upgrade: state export / import -------------------------------------
696
1048
  // The host persists state by calling export_state (readonly) and serializing
697
1049
  // the returned value to a code-independent blob. On a code upgrade it recreates
@@ -725,7 +1077,12 @@ application actor loads libraries
725
1077
  $delegation_cert -> core_state $delegation_cert,
726
1078
  $root_ad -> core_state $root_ad,
727
1079
  $root_profile -> core_state $root_profile,
728
- $contact_roots -> core_state $contact_roots
1080
+ $contact_roots -> core_state $contact_roots,
1081
+ $monitoring_enabled -> monitoring_enabled,
1082
+ $monitoring_inbox -> monitoring_inbox,
1083
+ $proxy_pending -> proxy_pending,
1084
+ $monitoring_proxy -> monitoring_proxy,
1085
+ $control_inbox -> control_inbox
729
1086
  ).
730
1087
  }
731
1088
 
@@ -808,6 +1165,29 @@ application actor loads libraries
808
1165
  pending_introductions -> (data $pending_introductions) safe (global_id ->> pending_intro_t).
809
1166
  }
810
1167
 
1168
+ // Monitoring + control state arrived after the local-book schema —
1169
+ // optional in old blobs the same way.
1170
+ if (data $monitoring_enabled) != NIL
1171
+ {
1172
+ monitoring_enabled -> (data $monitoring_enabled) safe bool.
1173
+ }
1174
+ if (data $monitoring_inbox) != NIL
1175
+ {
1176
+ monitoring_inbox -> (data $monitoring_inbox) safe (monitoring_copy_t[]).
1177
+ }
1178
+ if (data $proxy_pending) != NIL
1179
+ {
1180
+ proxy_pending -> (data $proxy_pending) safe proxy_pending_t.
1181
+ }
1182
+ if (data $monitoring_proxy) != NIL
1183
+ {
1184
+ monitoring_proxy -> (data $monitoring_proxy) safe proxy_binding_t.
1185
+ }
1186
+ if (data $control_inbox) != NIL
1187
+ {
1188
+ control_inbox -> (data $control_inbox) safe (control_req_t[]).
1189
+ }
1190
+
811
1191
  // Pending introducers' keys too: their channel to me predates approval,
812
1192
  // so it must survive an upgrade exactly like an approved contact's.
813
1193
  sc pending_introductions -- ( -> p)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adapt-toolkit/a2adapt",
3
- "version": "0.11.4",
3
+ "version": "0.11.5",
4
4
  "description": "MCP server daemon for a2adapt — one native ADAPT wrapper hosting N self-sovereign identities, exposing secure agent-to-agent messaging tools over HTTP (Streamable HTTP). Run `a2adapt-mcp start`.",
5
5
  "type": "module",
6
6
  "license": "MIT",