arc402-cli 0.7.0 → 0.7.2

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 (53) hide show
  1. package/dist/commands/backup.d.ts +3 -0
  2. package/dist/commands/backup.d.ts.map +1 -0
  3. package/dist/commands/backup.js +106 -0
  4. package/dist/commands/backup.js.map +1 -0
  5. package/dist/commands/daemon.d.ts.map +1 -1
  6. package/dist/commands/daemon.js +67 -0
  7. package/dist/commands/daemon.js.map +1 -1
  8. package/dist/commands/discover.d.ts.map +1 -1
  9. package/dist/commands/discover.js +60 -15
  10. package/dist/commands/discover.js.map +1 -1
  11. package/dist/commands/wallet.d.ts.map +1 -1
  12. package/dist/commands/wallet.js +136 -52
  13. package/dist/commands/wallet.js.map +1 -1
  14. package/dist/commands/watch.d.ts.map +1 -1
  15. package/dist/commands/watch.js +146 -9
  16. package/dist/commands/watch.js.map +1 -1
  17. package/dist/config.d.ts +8 -0
  18. package/dist/config.d.ts.map +1 -1
  19. package/dist/config.js +25 -1
  20. package/dist/config.js.map +1 -1
  21. package/dist/daemon/config.d.ts +15 -1
  22. package/dist/daemon/config.d.ts.map +1 -1
  23. package/dist/daemon/config.js +32 -0
  24. package/dist/daemon/config.js.map +1 -1
  25. package/dist/daemon/index.d.ts.map +1 -1
  26. package/dist/daemon/index.js +66 -21
  27. package/dist/daemon/index.js.map +1 -1
  28. package/dist/daemon/notify.d.ts +35 -6
  29. package/dist/daemon/notify.d.ts.map +1 -1
  30. package/dist/daemon/notify.js +176 -48
  31. package/dist/daemon/notify.js.map +1 -1
  32. package/dist/endpoint-notify.d.ts +2 -1
  33. package/dist/endpoint-notify.d.ts.map +1 -1
  34. package/dist/endpoint-notify.js +12 -3
  35. package/dist/endpoint-notify.js.map +1 -1
  36. package/dist/index.js +26 -0
  37. package/dist/index.js.map +1 -1
  38. package/dist/program.d.ts.map +1 -1
  39. package/dist/program.js +2 -0
  40. package/dist/program.js.map +1 -1
  41. package/package.json +1 -1
  42. package/src/commands/backup.ts +117 -0
  43. package/src/commands/daemon.ts +73 -0
  44. package/src/commands/discover.ts +74 -21
  45. package/src/commands/wallet.ts +137 -51
  46. package/src/commands/watch.ts +207 -10
  47. package/src/config.ts +39 -1
  48. package/src/daemon/config.ts +35 -0
  49. package/src/daemon/index.ts +65 -26
  50. package/src/daemon/notify.ts +199 -59
  51. package/src/endpoint-notify.ts +13 -3
  52. package/src/index.ts +26 -0
  53. package/src/program.ts +2 -0
@@ -19,7 +19,9 @@ import {
19
19
  DAEMON_SOCK,
20
20
  DAEMON_TOML,
21
21
  TEMPLATE_DAEMON_TOML,
22
+ loadDaemonConfig,
22
23
  } from "../daemon/config";
24
+ import { buildNotifier } from "../daemon/notify";
23
25
  import {
24
26
  buildOpenShellSecretExports,
25
27
  buildOpenShellSshConfig,
@@ -943,6 +945,77 @@ export function registerDaemonCommands(program: Command): void {
943
945
  console.log(" 5. Start the OpenShell-owned ARC-402 runtime: arc402 daemon start");
944
946
  });
945
947
 
948
+ // ── daemon notifications ──────────────────────────────────────────────────────
949
+ const notifications = daemon
950
+ .command("notifications")
951
+ .description("Show or test configured notification channels");
952
+
953
+ notifications
954
+ .command("show")
955
+ .description("Show all configured notification channels")
956
+ .action(() => {
957
+ if (!fs.existsSync(DAEMON_TOML)) {
958
+ console.error("daemon.toml not found. Run `arc402 daemon init` first.");
959
+ process.exit(1);
960
+ }
961
+ const cfg = loadDaemonConfig();
962
+ const notif = cfg.notifications;
963
+ const channels: string[] = [];
964
+
965
+ if (notif.telegram_bot_token && notif.telegram_chat_id) {
966
+ channels.push(`telegram chat_id=${notif.telegram_chat_id}`);
967
+ }
968
+ if (notif.discord?.webhook_url) {
969
+ const u = new URL(notif.discord.webhook_url);
970
+ channels.push(`discord ${u.hostname}${u.pathname.slice(0, 30)}...`);
971
+ }
972
+ if (notif.webhook?.url) {
973
+ channels.push(`webhook ${notif.webhook.url}`);
974
+ }
975
+ if (notif.email?.smtp_host && notif.email?.to) {
976
+ channels.push(`email ${notif.email.smtp_host}:${notif.email.smtp_port} → ${notif.email.to}`);
977
+ }
978
+
979
+ if (channels.length === 0) {
980
+ console.log("No notification channels configured.");
981
+ console.log("Edit ~/.arc402/daemon.toml to add Telegram, Discord, webhook, or email.");
982
+ } else {
983
+ console.log(`Configured channels (${channels.length}):`);
984
+ for (const ch of channels) console.log(` ${ch}`);
985
+ }
986
+ });
987
+
988
+ notifications
989
+ .command("test")
990
+ .description("Send a test message to all configured channels")
991
+ .action(async () => {
992
+ if (!fs.existsSync(DAEMON_TOML)) {
993
+ console.error("daemon.toml not found. Run `arc402 daemon init` first.");
994
+ process.exit(1);
995
+ }
996
+ const cfg = loadDaemonConfig();
997
+ const notifier = buildNotifier(cfg);
998
+ if (!notifier.isEnabled()) {
999
+ console.log("No notification channels configured. Nothing to test.");
1000
+ process.exit(0);
1001
+ }
1002
+ console.log("Sending test notification to all channels...");
1003
+ try {
1004
+ await notifier.send("daemon_started", "ARC-402 Test Notification",
1005
+ "This is a test message from arc402 daemon notifications test."
1006
+ );
1007
+ console.log("Test notification sent successfully.");
1008
+ } catch (err) {
1009
+ console.error(`Test notification failed: ${err instanceof Error ? err.message : String(err)}`);
1010
+ process.exit(1);
1011
+ }
1012
+ });
1013
+
1014
+ // Default action: show (arc402 daemon notifications → arc402 daemon notifications show)
1015
+ notifications.action(() => {
1016
+ notifications.help();
1017
+ });
1018
+
946
1019
  // ── daemon channel-watch ─────────────────────────────────────────────────────
947
1020
  daemon
948
1021
  .command("channel-watch")
@@ -5,6 +5,7 @@ import { loadConfig } from "../config";
5
5
  import { getClient } from "../client";
6
6
  import { getTrustTier, printTable, truncateAddress } from "../utils/format";
7
7
  import { c } from '../ui/colors';
8
+ import { renderTree } from '../ui/tree';
8
9
 
9
10
  // Minimal ABI for the new getAgentsWithCapability function (Spec 18)
10
11
  const CAPABILITY_REGISTRY_EXTRA_ABI = [
@@ -66,6 +67,21 @@ function computeCompositeScores(agents: Omit<ScoredAgent, "compositeScore" | "ra
66
67
  }));
67
68
  }
68
69
 
70
+ // ─── Endpoint health check ────────────────────────────────────────────────────
71
+
72
+ async function pingEndpoint(endpoint: string): Promise<"online" | "offline"> {
73
+ if (!endpoint || !/^https?:\/\//.test(endpoint)) return "offline";
74
+ try {
75
+ const ctrl = new AbortController();
76
+ const tid = setTimeout(() => ctrl.abort(), 3000);
77
+ const resp = await fetch(`${endpoint.replace(/\/$/, "")}/health`, { signal: ctrl.signal });
78
+ clearTimeout(tid);
79
+ return resp.ok ? "online" : "offline";
80
+ } catch {
81
+ return "offline";
82
+ }
83
+ }
84
+
69
85
  // ─── Command ──────────────────────────────────────────────────────────────────
70
86
 
71
87
  export function registerDiscoverCommand(program: Command): void {
@@ -84,6 +100,7 @@ export function registerDiscoverCommand(program: Command): void {
84
100
  .option("--top <n>", "Show top N agents by trust score")
85
101
  .option("--sort <field>", "Sort by: trust | price | jobs | stake | composite", "composite")
86
102
  .option("--limit <n>", "Max results", "20")
103
+ .option("--online", "Only show agents whose /health endpoint responds")
87
104
  .option("--json", "Machine-parseable output")
88
105
  .action(async (opts) => {
89
106
  const config = loadConfig();
@@ -264,34 +281,70 @@ export function registerDiscoverCommand(program: Command): void {
264
281
  // Assign 1-based ranks after sort
265
282
  scored = scored.slice(0, limit).map((a, i) => ({ ...a, rank: i + 1 }));
266
283
 
267
- // ── Step 6: Output ─────────────────────────────────────────────────────
284
+ // ── Step 6: Endpoint health checks ────────────────────────────────────
285
+
286
+ type ScoredWithStatus = ScoredAgent & { endpointStatus: "online" | "offline" | "unknown" };
287
+
288
+ let withStatus: ScoredWithStatus[];
289
+
290
+ if (opts.online || /* always ping for tree display */ true) {
291
+ const statuses = await Promise.all(
292
+ scored.map(async (agent) => {
293
+ if (!agent.endpoint) return "unknown" as const;
294
+ return pingEndpoint(agent.endpoint);
295
+ })
296
+ );
297
+ withStatus = scored.map((agent, i) => ({ ...agent, endpointStatus: statuses[i] as "online" | "offline" | "unknown" }));
298
+ } else {
299
+ withStatus = scored.map((agent) => ({ ...agent, endpointStatus: "unknown" as const }));
300
+ }
301
+
302
+ // Apply --online filter
303
+ if (opts.online) {
304
+ withStatus = withStatus.filter((a) => a.endpointStatus === "online");
305
+ if (withStatus.length === 0) {
306
+ console.log(`\n ${c.warning} No agents with responding /health endpoints found.`);
307
+ return;
308
+ }
309
+ }
310
+
311
+ // ── Step 7: Output ─────────────────────────────────────────────────────
268
312
 
269
313
  if (opts.json) {
270
314
  return console.log(JSON.stringify(
271
- scored,
315
+ withStatus,
272
316
  (_k, value) => typeof value === "bigint" ? value.toString() : value,
273
317
  2
274
318
  ));
275
319
  }
276
320
 
277
- console.log('\n ' + c.mark + c.white(' Discover Results') + c.dim(` ${scored.length} agent${scored.length !== 1 ? 's' : ''} found`));
278
- printTable(
279
- ["RANK", "ADDRESS", "NAME", "SERVICE", "TRUST", "SCORE", "CAPABILITIES"],
280
- scored.map((agent) => {
281
- const caps = (agent.canonicalCapabilities.length
282
- ? agent.canonicalCapabilities
283
- : agent.capabilities
284
- ).slice(0, 2).join(", ");
285
- return [
286
- String(agent.rank),
287
- truncateAddress(agent.wallet),
288
- agent.name,
289
- agent.serviceType,
290
- `${agent.trustScore} ${getTrustTier(agent.trustScore)}`,
291
- agent.compositeScore.toFixed(3),
292
- caps,
293
- ];
294
- })
295
- );
321
+ const onlineCount = withStatus.filter((a) => a.endpointStatus === "online").length;
322
+ console.log('\n ' + c.mark + c.white(' Discover Results') + c.dim(` — ${withStatus.length} agent${withStatus.length !== 1 ? 's' : ''} found, ${onlineCount} online`));
323
+
324
+ // Tree output per agent
325
+ for (const agent of withStatus) {
326
+ const caps = (agent.canonicalCapabilities.length
327
+ ? agent.canonicalCapabilities
328
+ : agent.capabilities
329
+ ).slice(0, 3).join(", ") || c.dim("none");
330
+
331
+ const statusIcon = agent.endpointStatus === "online"
332
+ ? c.green("● online")
333
+ : agent.endpointStatus === "offline"
334
+ ? c.red("○ offline")
335
+ : c.dim("? unknown");
336
+
337
+ const tierStr = getTrustTier(agent.trustScore);
338
+
339
+ console.log(`\n ${c.dim(`#${agent.rank}`)} ${c.white(agent.name)} ${c.dim(truncateAddress(agent.wallet))}`);
340
+ renderTree([
341
+ { label: "service", value: agent.serviceType },
342
+ { label: "trust", value: `${agent.trustScore} ${tierStr}` },
343
+ { label: "score", value: agent.compositeScore.toFixed(3) },
344
+ { label: "caps", value: caps },
345
+ { label: "endpoint", value: statusIcon, last: true },
346
+ ]);
347
+ }
348
+ console.log();
296
349
  });
297
350
  }
@@ -23,6 +23,27 @@ import { c } from "../ui/colors";
23
23
  import { formatAddress } from "../ui/format";
24
24
 
25
25
  const POLICY_ENGINE_DEFAULT = "0x44102e70c2A366632d98Fe40d892a2501fC7fFF2";
26
+ const GUARDIAN_KEY_PATH = path.join(os.homedir(), ".arc402", "guardian.key");
27
+
28
+ /** Save guardian private key to a restricted standalone file (never to config.json). */
29
+ function saveGuardianKey(privateKey: string): void {
30
+ fs.mkdirSync(path.dirname(GUARDIAN_KEY_PATH), { recursive: true, mode: 0o700 });
31
+ fs.writeFileSync(GUARDIAN_KEY_PATH, privateKey + "\n", { mode: 0o400 });
32
+ }
33
+
34
+ /** Load guardian private key from file, falling back to config for backwards compat. */
35
+ function loadGuardianKey(config: Arc402Config): string | null {
36
+ try {
37
+ return fs.readFileSync(GUARDIAN_KEY_PATH, "utf-8").trim();
38
+ } catch {
39
+ // Migration: if key still in config, migrate it to the file now
40
+ if (config.guardianPrivateKey) {
41
+ saveGuardianKey(config.guardianPrivateKey);
42
+ return config.guardianPrivateKey;
43
+ }
44
+ return null;
45
+ }
46
+ }
26
47
 
27
48
  function parseAmount(raw: string): bigint {
28
49
  const lower = raw.toLowerCase();
@@ -182,6 +203,9 @@ async function runCompleteOnboardingCeremony(
182
203
  );
183
204
  console.log(" " + c.success + " Machine key authorized");
184
205
  }
206
+ // Save progress after machine key step
207
+ config.onboardingProgress = { walletAddress, step: 2, completedSteps: ["machineKey"] };
208
+ saveConfig(config);
185
209
 
186
210
  // ── Step 3: Passkey ───────────────────────────────────────────────────────
187
211
  console.log("\n" + c.dim("── Step 3: Passkey (Face ID / WebAuthn) ──────────────────────"));
@@ -213,6 +237,9 @@ async function runCompleteOnboardingCeremony(
213
237
  console.log(" " + c.success + " Passkey set (via browser)");
214
238
  }
215
239
  }
240
+ // Save progress after passkey step
241
+ config.onboardingProgress = { walletAddress, step: 3, completedSteps: ["machineKey", "passkey"] };
242
+ saveConfig(config);
216
243
 
217
244
  // ── Step 4: Policy ────────────────────────────────────────────────────────
218
245
  console.log("\n" + c.dim("── Step 4: Policy ─────────────────────────────────────────────"));
@@ -340,6 +367,9 @@ async function runCompleteOnboardingCeremony(
340
367
  saveConfig(config);
341
368
 
342
369
  console.log(" " + c.success + " Policy configured");
370
+ // Save progress after policy step
371
+ config.onboardingProgress = { walletAddress, step: 4, completedSteps: ["machineKey", "passkey", "policy"] };
372
+ saveConfig(config);
343
373
 
344
374
  // ── Step 5: Agent Registration ─────────────────────────────────────────────
345
375
  console.log("\n" + c.dim("── Step 5: Agent Registration ─────────────────────────────────"));
@@ -428,6 +458,9 @@ async function runCompleteOnboardingCeremony(
428
458
  } else {
429
459
  console.log(" " + c.warning + " AgentRegistry address not configured — skipping");
430
460
  }
461
+ // Save progress after agent step, then clear on ceremony complete
462
+ config.onboardingProgress = { walletAddress, step: 5, completedSteps: ["machineKey", "passkey", "policy", "agent"] };
463
+ saveConfig(config);
431
464
 
432
465
  // ── Step 7: Workroom Init ─────────────────────────────────────────────────
433
466
  console.log("\n" + c.dim("── Step 7: Workroom ────────────────────────────────────────────"));
@@ -523,6 +556,10 @@ async function runCompleteOnboardingCeremony(
523
556
  ? c.white(agentEndpoint) + c.dim(` → localhost:${relayPort}`)
524
557
  : c.dim("—");
525
558
 
559
+ // Clear onboarding progress — ceremony complete
560
+ delete (config as unknown as Record<string, unknown>).onboardingProgress;
561
+ saveConfig(config);
562
+
526
563
  console.log("\n " + c.success + c.white(" Onboarding complete"));
527
564
  renderTree([
528
565
  { label: "Wallet", value: c.white(walletAddress) },
@@ -1066,19 +1103,54 @@ export function registerWalletCommands(program: Command): void {
1066
1103
  ? { botToken: config.telegramBotToken, chatId: config.telegramChatId, threadId: config.telegramThreadId }
1067
1104
  : undefined;
1068
1105
 
1106
+ // ── Resume check ──────────────────────────────────────────────────────
1107
+ const resumeProgress = config.onboardingProgress;
1108
+ const isResuming = !!(
1109
+ resumeProgress?.walletAddress &&
1110
+ resumeProgress.walletAddress === config.walletContractAddress &&
1111
+ config.ownerAddress
1112
+ );
1113
+ if (isResuming) {
1114
+ const stepNames: Record<number, string> = {
1115
+ 2: "machine key", 3: "passkey", 4: "policy setup", 5: "agent registration",
1116
+ };
1117
+ const nextStep = (resumeProgress!.step ?? 1) + 1;
1118
+ console.log(" " + c.dim(`◈ Resuming onboarding from step ${nextStep} (${stepNames[nextStep] ?? "ceremony"})...`));
1119
+ }
1120
+
1121
+ // ── Gas estimation ─────────────────────────────────────────────────────
1122
+ if (!isResuming) {
1123
+ let gasMsg = "~0.003 ETH (6 transactions on Base)";
1124
+ try {
1125
+ const feeData = await provider.getFeeData();
1126
+ const gasPrice = feeData.maxFeePerGas ?? feeData.gasPrice ?? BigInt(1_500_000_000);
1127
+ const deployGas = await provider.estimateGas({
1128
+ to: factoryAddress,
1129
+ data: factoryInterface.encodeFunctionData("createWallet", ["0x0000000071727De22E5E9d8BAf0edAc6f37da032"]),
1130
+ }).catch(() => BigInt(280_000));
1131
+ const ceremonyGas = BigInt(700_000); // ~5 ceremony txs × ~140k each
1132
+ const totalGasEth = parseFloat(ethers.formatEther((deployGas + ceremonyGas) * gasPrice));
1133
+ gasMsg = `~${totalGasEth.toFixed(4)} ETH (6 transactions on Base)`;
1134
+ } catch { /* use default */ }
1135
+ console.log(" " + c.dim(`◈ Estimated gas: ${gasMsg}`));
1136
+ }
1137
+
1069
1138
  // ── Step 1: Connect ────────────────────────────────────────────────────
1139
+ const connectPrompt = isResuming
1140
+ ? "Connect wallet to resume onboarding"
1141
+ : "Approve ARC402Wallet deployment — you will be set as owner";
1070
1142
  const { client, session, account } = await connectPhoneWallet(
1071
1143
  config.walletConnectProjectId,
1072
1144
  chainId,
1073
1145
  config,
1074
- { telegramOpts, prompt: "Approve ARC402Wallet deployment — you will be set as owner", hardware: !!opts.hardware }
1146
+ { telegramOpts, prompt: connectPrompt, hardware: !!opts.hardware }
1075
1147
  );
1076
1148
 
1077
1149
  const networkName = chainId === 8453 ? "Base" : "Base Sepolia";
1078
1150
  const shortAddr = `${account.slice(0, 6)}...${account.slice(-5)}`;
1079
1151
  console.log("\n" + c.success + c.white(` Connected: ${shortAddr} on ${networkName}`));
1080
1152
 
1081
- if (telegramOpts) {
1153
+ if (telegramOpts && !isResuming) {
1082
1154
  // Send "connected" message with a deploy confirmation button.
1083
1155
  // TODO: wire up full callback_data round-trip when a persistent bot process is available.
1084
1156
  await sendTelegramMessage({
@@ -1090,48 +1162,57 @@ export function registerWalletCommands(program: Command): void {
1090
1162
  });
1091
1163
  }
1092
1164
 
1093
- // ── Step 2: Confirm & Deploy ───────────────────────────────────────────
1094
- // WalletConnect approval already confirmed intent — sending automatically
1165
+ let walletAddress: string;
1095
1166
 
1096
- console.log("Deploying...");
1097
- const txHash = await sendTransactionWithSession(client, session, account, chainId, {
1098
- to: factoryAddress,
1099
- data: factoryInterface.encodeFunctionData("createWallet", ["0x0000000071727De22E5E9d8BAf0edAc6f37da032"]),
1100
- value: "0x0",
1101
- });
1167
+ if (isResuming) {
1168
+ // Resume: skip deploy, use existing wallet
1169
+ walletAddress = config.walletContractAddress!;
1170
+ console.log(" " + c.dim(`◈ Using existing wallet: ${walletAddress}`));
1171
+ } else {
1172
+ // ── Step 2: Confirm & Deploy ─────────────────────────────────────────
1173
+ // WalletConnect approval already confirmed intent — sending automatically
1102
1174
 
1103
- console.log(`\nTransaction submitted: ${txHash}`);
1104
- console.log("Waiting for confirmation...");
1105
- const receipt = await provider.waitForTransaction(txHash);
1106
- if (!receipt) {
1107
- console.error("Transaction not confirmed. Check on-chain.");
1108
- process.exit(1);
1109
- }
1110
- let walletAddress: string | null = null;
1111
- const factoryContract = new ethers.Contract(factoryAddress, WALLET_FACTORY_ABI, provider);
1112
- for (const log of receipt.logs) {
1113
- try {
1114
- const parsed = factoryContract.interface.parseLog(log);
1115
- if (parsed?.name === "WalletCreated") {
1116
- walletAddress = parsed.args.walletAddress as string;
1117
- break;
1118
- }
1119
- } catch { /* skip unparseable logs */ }
1120
- }
1121
- if (!walletAddress) {
1122
- console.error("Could not find WalletCreated event in receipt. Check the transaction on-chain.");
1123
- process.exit(1);
1175
+ console.log("Deploying...");
1176
+ const txHash = await sendTransactionWithSession(client, session, account, chainId, {
1177
+ to: factoryAddress,
1178
+ data: factoryInterface.encodeFunctionData("createWallet", ["0x0000000071727De22E5E9d8BAf0edAc6f37da032"]),
1179
+ value: "0x0",
1180
+ });
1181
+
1182
+ console.log(`\nTransaction submitted: ${txHash}`);
1183
+ console.log("Waiting for confirmation...");
1184
+ const receipt = await provider.waitForTransaction(txHash);
1185
+ if (!receipt) {
1186
+ console.error("Transaction not confirmed. Check on-chain.");
1187
+ process.exit(1);
1188
+ }
1189
+ let deployedWallet: string | null = null;
1190
+ const factoryContract = new ethers.Contract(factoryAddress, WALLET_FACTORY_ABI, provider);
1191
+ for (const log of receipt.logs) {
1192
+ try {
1193
+ const parsed = factoryContract.interface.parseLog(log);
1194
+ if (parsed?.name === "WalletCreated") {
1195
+ deployedWallet = parsed.args.walletAddress as string;
1196
+ break;
1197
+ }
1198
+ } catch { /* skip unparseable logs */ }
1199
+ }
1200
+ if (!deployedWallet) {
1201
+ console.error("Could not find WalletCreated event in receipt. Check the transaction on-chain.");
1202
+ process.exit(1);
1203
+ }
1204
+ walletAddress = deployedWallet;
1205
+ // ── Step 1 complete: save wallet + owner immediately ─────────────────
1206
+ config.walletContractAddress = walletAddress;
1207
+ config.ownerAddress = account;
1208
+ saveConfig(config);
1209
+ try { fs.chmodSync(getConfigPath(), 0o600); } catch { /* best-effort */ }
1210
+ console.log("\n " + c.success + c.white(" Wallet deployed"));
1211
+ renderTree([
1212
+ { label: "Wallet", value: walletAddress },
1213
+ { label: "Owner", value: account, last: true },
1214
+ ]);
1124
1215
  }
1125
- // ── Step 1 complete: save wallet + owner immediately ─────────────────
1126
- config.walletContractAddress = walletAddress;
1127
- config.ownerAddress = account;
1128
- saveConfig(config);
1129
- try { fs.chmodSync(getConfigPath(), 0o600); } catch { /* best-effort */ }
1130
- console.log("\n " + c.success + c.white(" Wallet deployed"));
1131
- renderTree([
1132
- { label: "Wallet", value: walletAddress },
1133
- { label: "Owner", value: account, last: true },
1134
- ]);
1135
1216
 
1136
1217
  // ── Steps 2–6: Complete onboarding ceremony (same WalletConnect session)
1137
1218
  const sendTxCeremony = async (
@@ -1175,8 +1256,10 @@ export function registerWalletCommands(program: Command): void {
1175
1256
  const guardianWallet = ethers.Wallet.createRandom();
1176
1257
  config.walletContractAddress = walletAddress;
1177
1258
  config.ownerAddress = address;
1178
- config.guardianPrivateKey = guardianWallet.privateKey;
1179
1259
  config.guardianAddress = guardianWallet.address;
1260
+ // Save key to restricted file — never store in config.json
1261
+ saveGuardianKey(guardianWallet.privateKey);
1262
+ if (config.guardianPrivateKey) delete (config as unknown as Record<string, unknown>).guardianPrivateKey;
1180
1263
  saveConfig(config);
1181
1264
 
1182
1265
  // Call setGuardian on the deployed wallet
@@ -1206,7 +1289,7 @@ export function registerWalletCommands(program: Command): void {
1206
1289
  { label: "Wallet", value: walletAddress },
1207
1290
  { label: "Guardian", value: guardianWallet.address, last: true },
1208
1291
  ]);
1209
- console.log(`Guardian private key saved to config (keep it safe used for emergency freeze only)`);
1292
+ console.log(`Guardian private key saved to ~/.arc402/guardian.key (chmod 400 — keep it safe, used for emergency freeze only)`);
1210
1293
  console.log(`Your wallet contract is ready for policy enforcement`);
1211
1294
  printOpenShellHint();
1212
1295
  }
@@ -1438,12 +1521,13 @@ export function registerWalletCommands(program: Command): void {
1438
1521
  console.error("walletContractAddress not set in config. Run `arc402 wallet deploy` first.");
1439
1522
  process.exit(1);
1440
1523
  }
1441
- if (!config.guardianPrivateKey) {
1442
- console.error("guardianPrivateKey not set in config. Guardian key was generated during `arc402 wallet deploy`.");
1524
+ const guardianKey = loadGuardianKey(config);
1525
+ if (!guardianKey) {
1526
+ console.error(`Guardian key not found. Expected at ~/.arc402/guardian.key (or guardianPrivateKey in config for legacy setups).`);
1443
1527
  process.exit(1);
1444
1528
  }
1445
1529
  const provider = new ethers.JsonRpcProvider(config.rpcUrl);
1446
- const guardianSigner = new ethers.Wallet(config.guardianPrivateKey, provider);
1530
+ const guardianSigner = new ethers.Wallet(guardianKey, provider);
1447
1531
  const walletContract = new ethers.Contract(config.walletContractAddress, ARC402_WALLET_GUARDIAN_ABI, guardianSigner);
1448
1532
 
1449
1533
  let tx;
@@ -1587,12 +1671,13 @@ export function registerWalletCommands(program: Command): void {
1587
1671
  });
1588
1672
 
1589
1673
  await provider.waitForTransaction(txHash);
1590
- config.guardianPrivateKey = guardianWallet.privateKey;
1674
+ saveGuardianKey(guardianWallet.privateKey);
1675
+ if (config.guardianPrivateKey) delete (config as unknown as Record<string, unknown>).guardianPrivateKey;
1591
1676
  config.guardianAddress = guardianWallet.address;
1592
1677
  saveConfig(config);
1593
1678
  console.log("\n" + c.success + c.white(` Guardian set to: ${guardianWallet.address}`));
1594
1679
  console.log(" " + c.dim("Tx:") + " " + c.white(txHash));
1595
- console.log(" " + c.dim("Guardian private key saved to config."));
1680
+ console.log(" " + c.dim("Guardian private key saved to ~/.arc402/guardian.key (chmod 400)."));
1596
1681
  console.log(" " + c.warning + " " + c.yellow("The guardian key can freeze your wallet. Store it separately from your hot key."));
1597
1682
  });
1598
1683
 
@@ -2419,9 +2504,10 @@ export function registerWalletCommands(program: Command): void {
2419
2504
  }
2420
2505
  }
2421
2506
 
2422
- // Persist guardian key if generated
2507
+ // Persist guardian key if generated — save to restricted file, not config.json
2423
2508
  if (guardianWallet) {
2424
- config.guardianPrivateKey = guardianWallet.privateKey;
2509
+ saveGuardianKey(guardianWallet.privateKey);
2510
+ if (config.guardianPrivateKey) delete (config as unknown as Record<string, unknown>).guardianPrivateKey;
2425
2511
  config.guardianAddress = guardianWallet.address;
2426
2512
  saveConfig(config);
2427
2513
  }
@@ -2433,7 +2519,7 @@ export function registerWalletCommands(program: Command): void {
2433
2519
  txHashes.forEach((h, i) => console.log(" " + c.dim(`Tx ${i + 1}:`) + " " + c.white(h)));
2434
2520
  }
2435
2521
  if (guardianWallet) {
2436
- console.log(" " + c.success + c.dim(` Guardian key saved to config — address: ${guardianWallet.address}`));
2522
+ console.log(" " + c.success + c.dim(` Guardian key saved to ~/.arc402/guardian.key — address: ${guardianWallet.address}`));
2437
2523
  console.log(" " + c.warning + " " + c.yellow("Store the guardian private key separately from your hot key."));
2438
2524
  }
2439
2525
  console.log(c.dim("\nVerify with: arc402 wallet status && arc402 wallet policy show"));