@uuaid/pillar 0.2.0-alpha.6 → 0.2.0-alpha.7

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.
Files changed (2) hide show
  1. package/bin/pillar.mjs +111 -28
  2. package/package.json +2 -2
package/bin/pillar.mjs CHANGED
@@ -3,6 +3,7 @@
3
3
  * pillar — one-command CLI.
4
4
  *
5
5
  * pillar init [--home ~/.pillar] first-time setup: mint UUAID + keys
6
+ * pillar init --refresh [--directory URL] re-sync carriers + address book (keychain untouched)
6
7
  * pillar id [--home ~/.pillar] print our UUAID + PeerId + addrs
7
8
  * pillar send <recipient> <kind> <json> send a signed envelope
8
9
  * pillar recv [--home ~/.pillar] [--follow] listen for envelopes and print them
@@ -89,9 +90,77 @@ function writeRuntimeFile(pillar, home) {
89
90
  writeFileSync(join(home, "runtime.json"), JSON.stringify(rt, null, 2));
90
91
  }
91
92
 
93
+ // Carrier list + address book merge from a fetched directory — shared by
94
+ // `init --directory` (first contact) and `init --refresh` (re-sync without
95
+ // touching the keychain).
96
+ function carriersFromDirectory(d) {
97
+ return [d.carrier, ...(Array.isArray(d.seeds) ? d.seeds.map((s) => s.url) : [])].filter(Boolean);
98
+ }
99
+
100
+ function mergeDirectoryAgents(home, d) {
101
+ if (!Array.isArray(d.agents)) return null;
102
+ const fleetPath = join(home, "fleet.json");
103
+ const prev = existsSync(fleetPath) ? JSON.parse(readFileSync(fleetPath, "utf-8")).fleet ?? {} : {};
104
+ const fleet = { ...prev };
105
+ let added = 0;
106
+ for (const a of d.agents) {
107
+ if (!a.uuaid || !a.publicKey) continue;
108
+ const key = typeof a.role === "string" && a.role ? a.role : a.uuaid;
109
+ fleet[key] = {
110
+ uuaid: a.uuaid,
111
+ publicKey: a.publicKey,
112
+ ...(prev[key]?.addrs ? { addrs: prev[key].addrs } : {}),
113
+ ...(prev[key]?.peerId ? { peerId: prev[key].peerId } : {}),
114
+ };
115
+ if (!prev[key]) added++;
116
+ }
117
+ writeFileSync(fleetPath, JSON.stringify({ fleet }, null, 2));
118
+ return { count: Object.keys(fleet).length, added };
119
+ }
120
+
121
+ async function fetchDirectory(url) {
122
+ const res = await fetch(url);
123
+ if (!res.ok) {
124
+ console.error(`directory fetch failed: ${url} -> ${res.status}`);
125
+ exit(1);
126
+ }
127
+ return res.json();
128
+ }
129
+
92
130
  async function cmdInit(args) {
93
131
  const home = homePath(args);
94
132
  mkdirSync(home, { recursive: true });
133
+
134
+ // --refresh re-syncs carriers + address book from a directory WITHOUT
135
+ // touching the keychain (init --force would mint a NEW identity and break
136
+ // every enrolled sender). Requires an existing identity.
137
+ if (args.refresh) {
138
+ const kcPath = join(home, "keychain.json");
139
+ if (!existsSync(kcPath)) {
140
+ console.error(`refresh: no keychain at ${kcPath} — run pillar init first (refresh never mints keys)`);
141
+ exit(1);
142
+ }
143
+ if (typeof args.directory !== "string") {
144
+ console.error("refresh: --directory <url> is required");
145
+ exit(1);
146
+ }
147
+ const d = await fetchDirectory(args.directory);
148
+ const list = carriersFromDirectory(d);
149
+ if (!list.length) {
150
+ console.error(`refresh: directory ${args.directory} lists no carrier and no seeds`);
151
+ exit(1);
152
+ }
153
+ const cfgPath = join(home, "config.json");
154
+ const cfg = existsSync(cfgPath) ? JSON.parse(readFileSync(cfgPath, "utf-8")) : {};
155
+ cfg.carriers = list;
156
+ writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));
157
+ console.log(`carriers refreshed from ${args.directory}: ${list.join(", ")}`);
158
+ const ab = mergeDirectoryAgents(home, d);
159
+ if (ab) console.log(`address book: ${ab.count} peers in ${join(home, "fleet.json")} (${ab.added} new from directory)`);
160
+ console.log(`refresh complete at ${home} (keychain untouched)`);
161
+ return;
162
+ }
163
+
95
164
  const kcPath = join(home, "keychain.json");
96
165
  if (existsSync(kcPath) && !args.force) {
97
166
  console.error(`keychain already exists at ${kcPath}. use --force to overwrite.`);
@@ -110,13 +179,8 @@ async function cmdInit(args) {
110
179
  // the primary carrier plus every seeds[] entry as carriers (the LimeWire
111
180
  // seed model — poll all, replicate to all); --carriers sets them directly.
112
181
  if (typeof args.directory === "string") {
113
- const res = await fetch(args.directory);
114
- if (!res.ok) {
115
- console.error(`init: directory fetch failed: ${args.directory} -> ${res.status}`);
116
- exit(1);
117
- }
118
- const d = await res.json();
119
- const list = [d.carrier, ...(Array.isArray(d.seeds) ? d.seeds.map(s => s.url) : [])].filter(Boolean);
182
+ const d = await fetchDirectory(args.directory);
183
+ const list = carriersFromDirectory(d);
120
184
  if (!list.length) {
121
185
  console.error(`init: directory ${args.directory} lists no carrier and no seeds`);
122
186
  exit(1);
@@ -126,25 +190,8 @@ async function cmdInit(args) {
126
190
  // Seed the address book from the directory's agents[] so a fresh agent can
127
191
  // `pillar ask`/`pillar send` with zero key plumbing (keys required for E2E).
128
192
  // Merge — never clobber a hand-curated fleet.json.
129
- if (Array.isArray(d.agents)) {
130
- const fleetPath = join(home, "fleet.json");
131
- const prev = existsSync(fleetPath) ? JSON.parse(readFileSync(fleetPath, "utf-8")).fleet ?? {} : {};
132
- const fleet = { ...prev };
133
- let added = 0;
134
- for (const a of d.agents) {
135
- if (!a.uuaid || !a.publicKey) continue;
136
- const key = typeof a.role === "string" && a.role ? a.role : a.uuaid;
137
- fleet[key] = {
138
- uuaid: a.uuaid,
139
- publicKey: a.publicKey,
140
- ...(prev[key]?.addrs ? { addrs: prev[key].addrs } : {}),
141
- ...(prev[key]?.peerId ? { peerId: prev[key].peerId } : {}),
142
- };
143
- if (!prev[key]) added++;
144
- }
145
- writeFileSync(fleetPath, JSON.stringify({ fleet }, null, 2));
146
- console.log(`address book: ${Object.keys(fleet).length} peers in ${fleetPath} (${added} new from directory)`);
147
- }
193
+ const ab = mergeDirectoryAgents(home, d);
194
+ if (ab) console.log(`address book: ${ab.count} peers in ${join(home, "fleet.json")} (${ab.added} new from directory)`);
148
195
  } else if (args.carriers) {
149
196
  cfg.carriers = args.carriers.split(",").map(s => s.trim()).filter(Boolean);
150
197
  }
@@ -238,6 +285,19 @@ async function cmdGroups(args) {
238
285
  }
239
286
  }
240
287
 
288
+ // Extract human-readable text from a zestro command_result. The server-side
289
+ // payload fields are unverified — defensively try the common shapes, cap at
290
+ // 8192 bytes (pillar-responder's MAX_REPLY_BYTES convention).
291
+ function replyTextFromResult(result) {
292
+ const text = result?.text ?? result?.result ?? result?.output ?? result?.response ?? result?.message;
293
+ if (typeof text === "string" && text.trim()) return text.slice(0, 8192);
294
+ if (typeof text !== "string" && text !== undefined && text !== null) {
295
+ const s = JSON.stringify(text);
296
+ if (s) return s.slice(0, 8192);
297
+ }
298
+ return JSON.stringify(result ?? {}).slice(0, 8192);
299
+ }
300
+
241
301
  async function cmdRecv(args) {
242
302
  const p = loadPillar(args);
243
303
  await p.start();
@@ -273,14 +333,37 @@ async function cmdRecv(args) {
273
333
  const line = { at: new Date().toISOString(), from: env.sender, kind: env.kind, payload: env.payload };
274
334
  if (env.payload?._group?.name) line.group = env.payload._group.name;
275
335
  console.log(JSON.stringify(line));
276
- if (bridge) {
336
+ if (bridge && env.kind === "request") {
337
+ // Interaction-convention return leg: ack FIRST — pillar-ask allows 120s
338
+ // per phase but ZestroBridge times out at 30s, so the ack goes out before
339
+ // the bridged agent turn starts. Then reply with the turn's result text,
340
+ // or status:"pending" on bridge timeout (command queued, turn running),
341
+ // or status:"error" with the bridge failure as reason.
342
+ const thread = env.payload?.thread ?? env.id;
343
+ const senderKey = env.transportSignature.publicKey; // open() F1: sender derives from this key
277
344
  try {
345
+ await p.send({ recipient: env.sender, recipientPublicKey: senderKey, kind: "ack", payload: { inReplyTo: env.id } });
278
346
  const result = await bridge.sendCommand({ command: JSON.stringify({ kind: env.kind, payload: env.payload, sender: env.sender }) });
279
- console.log(` → zestro result: ${JSON.stringify(result).slice(0, 200)}`);
347
+ const text = replyTextFromResult(result);
348
+ await p.send({ recipient: env.sender, recipientPublicKey: senderKey, kind: "reply", payload: { thread, inReplyTo: env.id, status: "ok", text } });
349
+ console.log(` → zestro reply sent (${text.length} bytes)`);
280
350
  } catch (e) {
281
351
  // command_result only arrives when the target agent's TURN completes —
282
352
  // a busy agent (long turn) exceeds any sane timeout while the command
283
353
  // is already queued in its tab. Timeout = accepted-unconfirmed, not failure.
354
+ if (/timed out/.test(e.message)) {
355
+ console.log(` → zestro: queued (command_result pending — agent busy)`);
356
+ await p.send({ recipient: env.sender, recipientPublicKey: senderKey, kind: "reply", payload: { thread, inReplyTo: env.id, status: "pending", text: "request forwarded to zestro agent — turn still running" } });
357
+ } else {
358
+ console.error(` → zestro bridge error: ${e.message}`);
359
+ await p.send({ recipient: env.sender, recipientPublicKey: senderKey, kind: "reply", payload: { thread, inReplyTo: env.id, status: "error", reason: e.message.slice(0, 300), text: `zestro bridge error: ${e.message.slice(0, 300)}` } });
360
+ }
361
+ }
362
+ } else if (bridge) {
363
+ try {
364
+ const result = await bridge.sendCommand({ command: JSON.stringify({ kind: env.kind, payload: env.payload, sender: env.sender }) });
365
+ console.log(` → zestro result: ${JSON.stringify(result).slice(0, 200)}`);
366
+ } catch (e) {
284
367
  if (/timed out/.test(e.message)) {
285
368
  console.log(` → zestro: queued (command_result pending — agent busy)`);
286
369
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uuaid/pillar",
3
- "version": "0.2.0-alpha.6",
3
+ "version": "0.2.0-alpha.7",
4
4
  "description": "IAASO-3050 reference implementation. Every agent runs its own PDP pillar, discovers peers via a Kademlia DHT, and exchanges cryptographically signed + Noise-encrypted messages over libp2p. One command to install and join the network.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -56,4 +56,4 @@
56
56
  "uint8arrays": "^5.1.1",
57
57
  "ws": "^8.21.3"
58
58
  }
59
- }
59
+ }