safehands-pharos 1.2.0 → 1.2.4
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.
- package/.env.example +26 -0
- package/README.md +311 -350
- package/contracts/RiskRegistry.json +75 -1
- package/contracts/RiskRegistry.sol +29 -1
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +91 -0
- package/dist/cli.js.map +1 -0
- package/dist/demo.d.ts +2 -0
- package/dist/demo.d.ts.map +1 -0
- package/dist/demo.js +172 -0
- package/dist/demo.js.map +1 -0
- package/dist/index.js +181 -169
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +2 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +66 -0
- package/dist/init.js.map +1 -0
- package/dist/lib/constants.d.ts +122 -7
- package/dist/lib/constants.d.ts.map +1 -1
- package/dist/lib/constants.js +139 -13
- package/dist/lib/constants.js.map +1 -1
- package/dist/lib/dodoApi.d.ts +14 -0
- package/dist/lib/dodoApi.d.ts.map +1 -1
- package/dist/lib/dodoApi.js +78 -22
- package/dist/lib/dodoApi.js.map +1 -1
- package/dist/lib/http.d.ts +15 -0
- package/dist/lib/http.d.ts.map +1 -0
- package/dist/lib/http.js +119 -0
- package/dist/lib/http.js.map +1 -0
- package/dist/lib/pharosClient.d.ts +4 -3
- package/dist/lib/pharosClient.d.ts.map +1 -1
- package/dist/lib/pharosClient.js +8 -5
- package/dist/lib/pharosClient.js.map +1 -1
- package/dist/lib/policy/actionPolicyEngine.d.ts +54 -0
- package/dist/lib/policy/actionPolicyEngine.d.ts.map +1 -0
- package/dist/lib/policy/actionPolicyEngine.js +213 -0
- package/dist/lib/policy/actionPolicyEngine.js.map +1 -0
- package/dist/lib/signer/index.d.ts +25 -0
- package/dist/lib/signer/index.d.ts.map +1 -0
- package/dist/lib/signer/index.js +90 -0
- package/dist/lib/signer/index.js.map +1 -0
- package/dist/lib/testDodoLive.d.ts +2 -0
- package/dist/lib/testDodoLive.d.ts.map +1 -0
- package/dist/lib/testDodoLive.js +105 -0
- package/dist/lib/testDodoLive.js.map +1 -0
- package/dist/lib/testLiveSafehands.d.ts +2 -0
- package/dist/lib/testLiveSafehands.d.ts.map +1 -0
- package/dist/lib/testLiveSafehands.js +93 -0
- package/dist/lib/testLiveSafehands.js.map +1 -0
- package/dist/lib/testRpcLive.d.ts +2 -0
- package/dist/lib/testRpcLive.d.ts.map +1 -0
- package/dist/lib/testRpcLive.js +89 -0
- package/dist/lib/testRpcLive.js.map +1 -0
- package/dist/lib/testTools.js +363 -354
- package/dist/lib/testTools.js.map +1 -1
- package/dist/lib/testX402Live.d.ts +2 -0
- package/dist/lib/testX402Live.d.ts.map +1 -0
- package/dist/lib/testX402Live.js +160 -0
- package/dist/lib/testX402Live.js.map +1 -0
- package/dist/lib/toolResponse.d.ts +26 -0
- package/dist/lib/toolResponse.d.ts.map +1 -0
- package/dist/lib/toolResponse.js +54 -0
- package/dist/lib/toolResponse.js.map +1 -0
- package/dist/lib/wallet/index.d.ts +19 -0
- package/dist/lib/wallet/index.d.ts.map +1 -0
- package/dist/lib/wallet/index.js +71 -0
- package/dist/lib/wallet/index.js.map +1 -0
- package/dist/tools/approveToken.d.ts +19 -20
- package/dist/tools/approveToken.d.ts.map +1 -1
- package/dist/tools/approveToken.js +44 -21
- package/dist/tools/approveToken.js.map +1 -1
- package/dist/tools/assessRisk.d.ts +22 -9
- package/dist/tools/assessRisk.d.ts.map +1 -1
- package/dist/tools/assessRisk.js +32 -9
- package/dist/tools/assessRisk.js.map +1 -1
- package/dist/tools/checkAllowance.d.ts +6 -6
- package/dist/tools/checkTokenSecurity.d.ts +9 -16
- package/dist/tools/checkTokenSecurity.d.ts.map +1 -1
- package/dist/tools/checkTokenSecurity.js +17 -22
- package/dist/tools/checkTokenSecurity.js.map +1 -1
- package/dist/tools/createAgentWallet.d.ts +27 -0
- package/dist/tools/createAgentWallet.d.ts.map +1 -0
- package/dist/tools/createAgentWallet.js +60 -0
- package/dist/tools/createAgentWallet.js.map +1 -0
- package/dist/tools/estimateGas.d.ts +31 -21
- package/dist/tools/estimateGas.d.ts.map +1 -1
- package/dist/tools/estimateGas.js +91 -95
- package/dist/tools/estimateGas.js.map +1 -1
- package/dist/tools/executeSwap.d.ts +13 -29
- package/dist/tools/executeSwap.d.ts.map +1 -1
- package/dist/tools/executeSwap.js +68 -46
- package/dist/tools/executeSwap.js.map +1 -1
- package/dist/tools/explainRisk.d.ts +30 -0
- package/dist/tools/explainRisk.d.ts.map +1 -0
- package/dist/tools/explainRisk.js +33 -0
- package/dist/tools/explainRisk.js.map +1 -0
- package/dist/tools/getAgentWallet.d.ts +22 -0
- package/dist/tools/getAgentWallet.d.ts.map +1 -0
- package/dist/tools/getAgentWallet.js +28 -0
- package/dist/tools/getAgentWallet.js.map +1 -0
- package/dist/tools/getAgentWalletBalance.d.ts +12 -0
- package/dist/tools/getAgentWalletBalance.d.ts.map +1 -0
- package/dist/tools/getAgentWalletBalance.js +71 -0
- package/dist/tools/getAgentWalletBalance.js.map +1 -0
- package/dist/tools/getExecutionHistory.d.ts +4 -4
- package/dist/tools/getGasPrice.d.ts +26 -8
- package/dist/tools/getGasPrice.d.ts.map +1 -1
- package/dist/tools/getGasPrice.js +43 -35
- package/dist/tools/getGasPrice.js.map +1 -1
- package/dist/tools/getPoolInfo.d.ts +47 -59
- package/dist/tools/getPoolInfo.d.ts.map +1 -1
- package/dist/tools/getPoolInfo.js +96 -57
- package/dist/tools/getPoolInfo.js.map +1 -1
- package/dist/tools/getTokenPrice.d.ts +95 -9
- package/dist/tools/getTokenPrice.d.ts.map +1 -1
- package/dist/tools/getTokenPrice.js +95 -56
- package/dist/tools/getTokenPrice.js.map +1 -1
- package/dist/tools/getWalletBalance.d.ts +40 -11
- package/dist/tools/getWalletBalance.d.ts.map +1 -1
- package/dist/tools/getWalletBalance.js +64 -47
- package/dist/tools/getWalletBalance.js.map +1 -1
- package/dist/tools/publishRiskScore.d.ts +12 -10
- package/dist/tools/publishRiskScore.d.ts.map +1 -1
- package/dist/tools/publishRiskScore.js +33 -19
- package/dist/tools/publishRiskScore.js.map +1 -1
- package/dist/tools/queryRiskRegistry.d.ts +3 -3
- package/dist/tools/safehandsPreflightCheck.d.ts +78 -0
- package/dist/tools/safehandsPreflightCheck.d.ts.map +1 -0
- package/dist/tools/safehandsPreflightCheck.js +48 -0
- package/dist/tools/safehandsPreflightCheck.js.map +1 -0
- package/dist/tools/safehandsRiskReport.d.ts +82 -0
- package/dist/tools/safehandsRiskReport.d.ts.map +1 -0
- package/dist/tools/safehandsRiskReport.js +29 -0
- package/dist/tools/safehandsRiskReport.js.map +1 -0
- package/dist/tools/safehandsSafeExecute.d.ts +21 -0
- package/dist/tools/safehandsSafeExecute.d.ts.map +1 -0
- package/dist/tools/safehandsSafeExecute.js +76 -0
- package/dist/tools/safehandsSafeExecute.js.map +1 -0
- package/dist/tools/safehandsWalletHealth.d.ts +15 -0
- package/dist/tools/safehandsWalletHealth.d.ts.map +1 -0
- package/dist/tools/safehandsWalletHealth.js +104 -0
- package/dist/tools/safehandsWalletHealth.js.map +1 -0
- package/dist/tools/safehandsX402Preflight.d.ts +27 -0
- package/dist/tools/safehandsX402Preflight.d.ts.map +1 -0
- package/dist/tools/safehandsX402Preflight.js +66 -0
- package/dist/tools/safehandsX402Preflight.js.map +1 -0
- package/dist/tools/sendPayment.d.ts +13 -35
- package/dist/tools/sendPayment.d.ts.map +1 -1
- package/dist/tools/sendPayment.js +53 -47
- package/dist/tools/sendPayment.js.map +1 -1
- package/dist/tools/simulateTransaction.d.ts +4 -4
- package/dist/tools/tokenRegistryStatus.d.ts +27 -0
- package/dist/tools/tokenRegistryStatus.d.ts.map +1 -0
- package/dist/tools/tokenRegistryStatus.js +97 -0
- package/dist/tools/tokenRegistryStatus.js.map +1 -0
- package/dist/tools/x402PayAndFetch.d.ts +40 -16
- package/dist/tools/x402PayAndFetch.d.ts.map +1 -1
- package/dist/tools/x402PayAndFetch.js +115 -47
- package/dist/tools/x402PayAndFetch.js.map +1 -1
- package/dist/x402Server.js +149 -115
- package/dist/x402Server.js.map +1 -1
- package/examples/pharos-skill-engine/SKILL.safehands.md +85 -0
- package/examples/pharos-skill-engine/assets/safehands/example-actions.json +49 -0
- package/examples/pharos-skill-engine/assets/safehands/policy-defaults.json +11 -0
- package/examples/pharos-skill-engine/references/safehands.md +345 -0
- package/examples/scenario-hack.ts +38 -0
- package/package.json +19 -5
- package/skill/SKILL.md +127 -0
- package/skill/assets/safehands/example-actions.json +49 -0
- package/skill/assets/safehands/policy-defaults.json +11 -0
- package/skill/references/safehands.md +345 -0
- package/.agents/skill/safehands/SKILL.md +0 -200
- package/.agents/skill/safehands/assets/networks.json +0 -24
- package/.agents/skill/safehands/assets/tokens.json +0 -60
package/dist/lib/testTools.js
CHANGED
|
@@ -1,389 +1,398 @@
|
|
|
1
|
-
// ─── SafeHands —
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import { privateKeyToAccount } from "viem/accounts";
|
|
1
|
+
// ─── SafeHands — Non-destructive smoke test for tool handlers ───────────
|
|
2
|
+
// This script intentionally avoids broadcasting transactions. It checks that
|
|
3
|
+
// tool handlers can be invoked safely and return predictable AI-readable data
|
|
4
|
+
// or structured failures when external services/env vars are unavailable.
|
|
5
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
7
6
|
import express from "express";
|
|
8
|
-
import { x402Facilitator } from "@x402/core/facilitator";
|
|
9
|
-
import { toFacilitatorEvmSigner } from "@x402/evm";
|
|
10
|
-
import { ExactEvmScheme as FacilitatorExactEvmScheme } from "@x402/evm/exact/facilitator";
|
|
11
|
-
import { ExactEvmScheme as ServerExactEvmScheme } from "@x402/evm/exact/server";
|
|
12
|
-
import { HTTPFacilitatorClient } from "@x402/core/server";
|
|
13
|
-
import { paymentMiddleware, x402ResourceServer } from "@x402/express";
|
|
14
7
|
import { handleAssessRisk } from "../tools/assessRisk.js";
|
|
15
8
|
import { handleExecuteSwap } from "../tools/executeSwap.js";
|
|
16
9
|
import { handleSendPayment } from "../tools/sendPayment.js";
|
|
17
10
|
import { handleSimulateTransaction } from "../tools/simulateTransaction.js";
|
|
18
|
-
import { handleGetExecutionHistory } from "../tools/getExecutionHistory.js";
|
|
19
11
|
import { handleGetTokenPrice } from "../tools/getTokenPrice.js";
|
|
20
12
|
import { handleGetWalletBalance } from "../tools/getWalletBalance.js";
|
|
21
|
-
import { handleCheckAllowance } from "../tools/checkAllowance.js";
|
|
22
|
-
import { handleGetTransactionStatus } from "../tools/getTransactionStatus.js";
|
|
23
13
|
import { handleEstimateGas } from "../tools/estimateGas.js";
|
|
24
14
|
import { handlePublishRiskScore } from "../tools/publishRiskScore.js";
|
|
25
|
-
import { handleQueryRiskRegistry } from "../tools/queryRiskRegistry.js";
|
|
26
15
|
import { handleApproveToken } from "../tools/approveToken.js";
|
|
27
|
-
import { handleGetGasPrice } from "../tools/getGasPrice.js";
|
|
28
|
-
import { handleGetPoolInfo } from "../tools/getPoolInfo.js";
|
|
29
16
|
import { handleCheckTokenSecurity } from "../tools/checkTokenSecurity.js";
|
|
30
17
|
import { handleX402PayAndFetch } from "../tools/x402PayAndFetch.js";
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
18
|
+
import { fail, ok } from "./toolResponse.js";
|
|
19
|
+
import { handleSafeHandsPreflightCheck } from "../tools/safehandsPreflightCheck.js";
|
|
20
|
+
import { handleSafeHandsX402Preflight } from "../tools/safehandsX402Preflight.js";
|
|
21
|
+
import { handleSafeHandsWalletHealth } from "../tools/safehandsWalletHealth.js";
|
|
22
|
+
import { handleSafeHandsRiskReport } from "../tools/safehandsRiskReport.js";
|
|
23
|
+
import { handleExplainRisk } from "../tools/explainRisk.js";
|
|
24
|
+
import { handleTokenRegistryStatus } from "../tools/tokenRegistryStatus.js";
|
|
25
|
+
import { handleCreateAgentWallet } from "../tools/createAgentWallet.js";
|
|
26
|
+
import { handleGetAgentWallet } from "../tools/getAgentWallet.js";
|
|
27
|
+
import { getSigner, isSignerFailure } from "./signer/index.js";
|
|
28
|
+
import { USDC_ADDRESS } from "./constants.js";
|
|
29
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
30
|
+
import { join } from "node:path";
|
|
31
|
+
import { spawnSync } from "node:child_process";
|
|
32
|
+
const WALLET = process.env.WALLET_ADDRESS || "0x0000000000000000000000000000000000000001";
|
|
33
|
+
const RECIPIENT = "0x000000000000000000000000000000000000dEaD";
|
|
34
|
+
const TEST_TX = "0x0000000000000000000000000000000000000000000000000000000000000000";
|
|
35
|
+
const rows = [];
|
|
36
|
+
function isStructured(value) {
|
|
37
|
+
return !!value && typeof value === "object" && "success" in value && "error" in value && "timestamp" in value;
|
|
45
38
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
console.log(`1. assess_risk → score=${r.riskScore} level=${r.riskLevel}`);
|
|
64
|
-
ok("assess_risk", `score=${r.riskScore}, ${r.riskLevel}, ${r.recommendation}`);
|
|
65
|
-
}
|
|
66
|
-
catch (e) {
|
|
67
|
-
console.log(`1. assess_risk → ❌`);
|
|
68
|
-
fail("assess_risk", e.message);
|
|
69
|
-
}
|
|
70
|
-
// 2. simulate_transaction (swap)
|
|
71
|
-
try {
|
|
72
|
-
const r = await handleSimulateTransaction({ action: "swap", tokenIn: "PHRS", tokenOut: "USDC", amount: "0.01" });
|
|
73
|
-
console.log(`2. simulate_tx (swap) → success=${r.wouldSucceed} gas=${r.gasEstimate}`);
|
|
74
|
-
ok("simulate_transaction", `wouldSucceed=${r.wouldSucceed}, gas=${r.gasEstimate}`);
|
|
75
|
-
}
|
|
76
|
-
catch (e) {
|
|
77
|
-
console.log(`2. simulate_tx → ❌`);
|
|
78
|
-
fail("simulate_transaction", e.message);
|
|
79
|
-
}
|
|
80
|
-
// 3. simulate_transaction (transfer)
|
|
81
|
-
try {
|
|
82
|
-
const r = await handleSimulateTransaction({ action: "transfer", amount: "0.001", toAddress: "0x000000000000000000000000000000000000dEaD" });
|
|
83
|
-
console.log(`3. simulate_tx (xfer) → success=${r.wouldSucceed} gas=${r.gasEstimate}`);
|
|
84
|
-
ok("simulate_tx (xfer)", `wouldSucceed=${r.wouldSucceed}, gas=${r.gasEstimate}`);
|
|
85
|
-
}
|
|
86
|
-
catch (e) {
|
|
87
|
-
console.log(`3. simulate_tx (xfer) → ❌`);
|
|
88
|
-
fail("simulate_tx (xfer)", e.message);
|
|
89
|
-
}
|
|
90
|
-
// 4. get_token_price
|
|
91
|
-
try {
|
|
92
|
-
const r = await handleGetTokenPrice({ token: "PHRS" });
|
|
93
|
-
console.log(`4. get_token_price → PHRS=$${r.priceUsd}`);
|
|
94
|
-
ok("get_token_price", `PHRS=$${r.priceUsd}`);
|
|
95
|
-
}
|
|
96
|
-
catch (e) {
|
|
97
|
-
console.log(`4. get_token_price → ❌`);
|
|
98
|
-
fail("get_token_price", e.message);
|
|
99
|
-
}
|
|
100
|
-
// 5. get_wallet_balance
|
|
101
|
-
try {
|
|
102
|
-
const r = await handleGetWalletBalance({ walletAddress: WALLET });
|
|
103
|
-
console.log(`5. get_wallet_balance → PHRS=${r.balances.PHRS.balance} USDC=${r.balances.USDC.balance} USDT=${r.balances.USDT.balance}`);
|
|
104
|
-
ok("get_wallet_balance", `PHRS=${r.balances.PHRS.balance}, USDC=${r.balances.USDC.balance}, total=$${r.totalUsd}`);
|
|
105
|
-
}
|
|
106
|
-
catch (e) {
|
|
107
|
-
console.log(`5. get_wallet_balance → ❌`);
|
|
108
|
-
fail("get_wallet_balance", e.message);
|
|
109
|
-
}
|
|
110
|
-
// 6. check_allowance
|
|
111
|
-
try {
|
|
112
|
-
const r = await handleCheckAllowance({ token: "USDC" });
|
|
113
|
-
console.log(`6. check_allowance → USDC allowance=${r.allowance} needsApproval=${r.needsApproval}`);
|
|
114
|
-
ok("check_allowance", `allowance=${r.allowance}, needsApproval=${r.needsApproval}`);
|
|
115
|
-
}
|
|
116
|
-
catch (e) {
|
|
117
|
-
console.log(`6. check_allowance → ❌`);
|
|
118
|
-
fail("check_allowance", e.message);
|
|
119
|
-
}
|
|
120
|
-
// 7. estimate_gas (swap)
|
|
121
|
-
try {
|
|
122
|
-
const r = await handleEstimateGas({ walletAddress: WALLET, action: "swap", tokenIn: "PHRS", tokenOut: "USDC", amount: "0.01" });
|
|
123
|
-
console.log(`7. estimate_gas → gas=${r.gasEstimate} cost=${r.gasCostPHRS} PHRS`);
|
|
124
|
-
ok("estimate_gas", `gas=${r.gasEstimate}, cost=${r.gasCostPHRS} PHRS, sufficient=${r.isSufficient}`);
|
|
125
|
-
}
|
|
126
|
-
catch (e) {
|
|
127
|
-
console.log(`7. estimate_gas → ❌`);
|
|
128
|
-
fail("estimate_gas", e.message);
|
|
129
|
-
}
|
|
130
|
-
// 8. get_gas_price
|
|
131
|
-
try {
|
|
132
|
-
const r = await handleGetGasPrice({});
|
|
133
|
-
console.log(`8. get_gas_price → ${r.gasPriceGwei} Gwei, trend=${r.trend}`);
|
|
134
|
-
ok("get_gas_price", `${r.gasPriceGwei} Gwei, trend=${r.trend}`);
|
|
135
|
-
}
|
|
136
|
-
catch (e) {
|
|
137
|
-
console.log(`8. get_gas_price → ❌`);
|
|
138
|
-
fail("get_gas_price", e.message);
|
|
139
|
-
}
|
|
140
|
-
// 9. get_pool_info
|
|
141
|
-
try {
|
|
142
|
-
const r = await handleGetPoolInfo({ tokenA: "PHRS", tokenB: "USDC" });
|
|
143
|
-
const ratio = r.available ? r.priceRatio : "no pool";
|
|
144
|
-
console.log(`9. get_pool_info → ${ratio}`);
|
|
145
|
-
ok("get_pool_info", `available=${r.available}, ${ratio}`);
|
|
146
|
-
}
|
|
147
|
-
catch (e) {
|
|
148
|
-
console.log(`9. get_pool_info → ❌`);
|
|
149
|
-
fail("get_pool_info", e.message);
|
|
150
|
-
}
|
|
151
|
-
// 10. execute_swap (LIVE — 0.01 PHRS → USDC)
|
|
152
|
-
try {
|
|
153
|
-
const r = await handleExecuteSwap({ tokenIn: "PHRS", tokenOut: "USDC", amountIn: "0.01" });
|
|
154
|
-
if (r.success) {
|
|
155
|
-
swapTxHash = r.txHash;
|
|
156
|
-
console.log(`10. execute_swap → ✅ out=${r.amountOut} USDC tx=${r.txHash.slice(0, 18)}...`);
|
|
157
|
-
ok("execute_swap", `out=${r.amountOut} USDC, tx=${r.txHash}`);
|
|
158
|
-
}
|
|
159
|
-
else {
|
|
160
|
-
console.log(`10. execute_swap → not executed: ${r.error}`);
|
|
161
|
-
ok("execute_swap", `handled: ${r.error}`);
|
|
39
|
+
function normalize(value) {
|
|
40
|
+
if (isStructured(value))
|
|
41
|
+
return value;
|
|
42
|
+
return ok(value);
|
|
43
|
+
}
|
|
44
|
+
function readAllToolSourceFiles() {
|
|
45
|
+
const root = join(process.cwd(), "src", "tools");
|
|
46
|
+
const out = [];
|
|
47
|
+
function walk(dir) {
|
|
48
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
49
|
+
const full = join(dir, entry.name);
|
|
50
|
+
if (entry.isDirectory())
|
|
51
|
+
walk(full);
|
|
52
|
+
else if (entry.isFile() && entry.name.endsWith(".ts")) {
|
|
53
|
+
const rel = full.replace(process.cwd(), "").replace(/^[\\/]/, "").replace(/\\/g, "/");
|
|
54
|
+
out.push({ path: rel, content: readFileSync(full, "utf8") });
|
|
55
|
+
}
|
|
162
56
|
}
|
|
163
57
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
58
|
+
walk(root);
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
function runBuiltCli(args) {
|
|
62
|
+
const entry = join(process.cwd(), "dist", "index.js");
|
|
63
|
+
if (!existsSync(entry))
|
|
64
|
+
return fail("CLI_DIST_MISSING", "Run npm run build before CLI smoke tests.", false, "cli_smoke");
|
|
65
|
+
const result = spawnSync(process.execPath, [entry, ...args], {
|
|
66
|
+
cwd: process.cwd(),
|
|
67
|
+
env: { ...process.env, WALLET_MODE: "none", WRITE_TOOLS_ENABLED: "false", PRIVATE_KEY: "" },
|
|
68
|
+
encoding: "utf8",
|
|
69
|
+
});
|
|
70
|
+
const stdout = result.stdout.trim();
|
|
169
71
|
try {
|
|
170
|
-
|
|
171
|
-
if (r.success) {
|
|
172
|
-
console.log(`11. send_payment → ✅ tx=${r.txHash.slice(0, 18)}...`);
|
|
173
|
-
ok("send_payment", `sent 0.001 PHRS, tx=${r.txHash}`);
|
|
174
|
-
}
|
|
175
|
-
else {
|
|
176
|
-
console.log(`11. send_payment → not sent: ${r.error}`);
|
|
177
|
-
ok("send_payment", `handled: ${r.error}`);
|
|
178
|
-
}
|
|
72
|
+
return JSON.parse(stdout);
|
|
179
73
|
}
|
|
180
|
-
catch
|
|
181
|
-
|
|
182
|
-
fail("send_payment", e.message);
|
|
74
|
+
catch {
|
|
75
|
+
return fail("CLI_INVALID_STDOUT", `exit=${result.status} stdout=${stdout.slice(0, 240)} stderr=${result.stderr.slice(0, 240)}`, false, "cli_smoke");
|
|
183
76
|
}
|
|
184
|
-
|
|
77
|
+
}
|
|
78
|
+
function runBuiltCliRaw(args) {
|
|
79
|
+
const entry = join(process.cwd(), "dist", "index.js");
|
|
80
|
+
if (!existsSync(entry))
|
|
81
|
+
return { status: 127, stdout: "", stderr: "dist/index.js missing" };
|
|
82
|
+
const result = spawnSync(process.execPath, [entry, ...args], {
|
|
83
|
+
cwd: process.cwd(),
|
|
84
|
+
env: { ...process.env, WALLET_MODE: "none", WRITE_TOOLS_ENABLED: "false", PRIVATE_KEY: "", X402_SIGNER_PRIVATE_KEY: "" },
|
|
85
|
+
encoding: "utf8",
|
|
86
|
+
timeout: 60_000,
|
|
87
|
+
});
|
|
88
|
+
return { status: result.status, stdout: result.stdout, stderr: result.stderr };
|
|
89
|
+
}
|
|
90
|
+
function runNpmPackDryRun() {
|
|
91
|
+
const result = spawnSync("npm", ["pack", "--dry-run", "--json"], {
|
|
92
|
+
cwd: process.cwd(),
|
|
93
|
+
encoding: "utf8",
|
|
94
|
+
timeout: 60_000,
|
|
95
|
+
shell: true,
|
|
96
|
+
});
|
|
97
|
+
if (result.status !== 0 || result.error)
|
|
98
|
+
return fail("NPM_PACK_DRY_RUN_FAILED", result.error?.message || result.stderr || result.stdout, false, "npm_pack");
|
|
185
99
|
try {
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
100
|
+
const parsed = JSON.parse(result.stdout.trim());
|
|
101
|
+
const files = (parsed[0]?.files || []).map((f) => f.path);
|
|
102
|
+
const unsafeExact = new Set([".env", ".env.local", "wallet-store.json"]);
|
|
103
|
+
const unsafe = files.filter((file) => unsafeExact.has(file) ||
|
|
104
|
+
file.endsWith(".pem") ||
|
|
105
|
+
file.endsWith(".key") ||
|
|
106
|
+
file.includes("node_modules/") ||
|
|
107
|
+
file.startsWith("logs/") ||
|
|
108
|
+
file.endsWith(".log"));
|
|
109
|
+
return unsafe.length ? fail("NPM_PACK_UNSAFE_FILES", unsafe.join(", "), false, "npm_pack") : ok({ totalFiles: files.length, unsafe, includesEnvExample: files.includes(".env.example") });
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
return fail("NPM_PACK_PARSE_FAILED", err instanceof Error ? err.message : String(err), false, "npm_pack");
|
|
195
113
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
114
|
+
}
|
|
115
|
+
function hasAll(text, parts) {
|
|
116
|
+
return parts.every((part) => text.includes(part));
|
|
117
|
+
}
|
|
118
|
+
function summarize(value) {
|
|
119
|
+
if (!value.success)
|
|
120
|
+
return `${value.error.code}: ${value.error.message}`;
|
|
121
|
+
if (value.data && typeof value.data === "object") {
|
|
122
|
+
const keys = Object.keys(value.data).slice(0, 6).join(", ");
|
|
123
|
+
return `success data keys: ${keys || "none"}`;
|
|
124
|
+
}
|
|
125
|
+
return "success";
|
|
126
|
+
}
|
|
127
|
+
async function record(tool, fn, expect) {
|
|
128
|
+
try {
|
|
129
|
+
const result = normalize(await fn());
|
|
130
|
+
const passed = expect ? expect(result) : true;
|
|
131
|
+
rows.push({ tool, status: passed ? "PASS" : "FAIL", note: summarize(result) });
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
const result = fail("SMOKE_TEST_HANDLER_THROW", err instanceof Error ? err.message : String(err), false, tool);
|
|
135
|
+
const passed = expect ? expect(result) : false;
|
|
136
|
+
rows.push({
|
|
137
|
+
tool,
|
|
138
|
+
status: passed ? "PASS" : "FAIL",
|
|
139
|
+
note: summarize(result),
|
|
140
|
+
});
|
|
199
141
|
}
|
|
200
|
-
|
|
142
|
+
}
|
|
143
|
+
async function withLocalServer(handler) {
|
|
144
|
+
const app = express();
|
|
145
|
+
app.get("/supported", (_req, res) => {
|
|
146
|
+
res.json({ ok: true, kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:688689" }] });
|
|
147
|
+
});
|
|
148
|
+
const server = await new Promise((resolve) => {
|
|
149
|
+
const instance = app.listen(0, "127.0.0.1", () => resolve(instance));
|
|
150
|
+
});
|
|
201
151
|
try {
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
152
|
+
const address = server.address();
|
|
153
|
+
if (!address || typeof address === "string")
|
|
154
|
+
throw new Error("Failed to open local test server");
|
|
155
|
+
return await handler(`http://127.0.0.1:${address.port}/supported`);
|
|
206
156
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
157
|
+
finally {
|
|
158
|
+
if ("closeAllConnections" in server)
|
|
159
|
+
server.closeAllConnections();
|
|
160
|
+
await new Promise((resolve, reject) => server.close((err) => (err ? reject(err) : resolve())));
|
|
210
161
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
162
|
+
}
|
|
163
|
+
async function run() {
|
|
164
|
+
console.log("\n🛡️ SafeHands — non-destructive tool smoke test");
|
|
165
|
+
console.log(`Wallet used for dry/smoke checks: ${WALLET}`);
|
|
166
|
+
await record("assess_risk_validation", () => handleAssessRisk({ action: "swap", amount: "0.001", walletAddress: WALLET }), (res) => !res.success && res.error.code === "SMOKE_TEST_HANDLER_THROW");
|
|
167
|
+
await record("simulate_transaction_validation", () => handleSimulateTransaction({ action: "transfer", amount: "0.001", walletAddress: WALLET }));
|
|
168
|
+
await record("get_token_price_public_mode", () => handleGetTokenPrice({ token: "PHRS" }), (res) => (res.success && typeof res.data === "object" && res.data !== null && "publicMode" in res.data) ||
|
|
169
|
+
(!res.success && ["DODO_API_AUTH_REQUIRED", "DODO_API_UNAVAILABLE", "DODO_API_TIMEOUT", "DODO_API_RATE_LIMITED"].includes(res.error.code)));
|
|
170
|
+
await record("get_wallet_balance_invalid_wallet", () => handleGetWalletBalance({ walletAddress: "not-an-address" }), (res) => !res.success && res.error.code === "INVALID_WALLET_ADDRESS");
|
|
171
|
+
await record("estimate_gas_invalid_wallet", () => handleEstimateGas({ walletAddress: "not-an-address", action: "transfer", amount: "0.001", toAddress: RECIPIENT }), (res) => !res.success && res.error.code === "INVALID_WALLET_ADDRESS");
|
|
172
|
+
await record("check_token_security_invalid_address", () => handleCheckTokenSecurity({ tokenAddress: "not-an-address" }), (res) => !res.success && res.error.code === "INVALID_TOKEN_ADDRESS");
|
|
173
|
+
await record("execute_swap_guard", () => handleExecuteSwap({ tokenIn: "PHRS", tokenOut: "USDC", amountIn: "0.001" }), (res) => !res.success && res.error.code === "WRITE_TOOLS_DISABLED");
|
|
174
|
+
await record("send_payment_guard", () => handleSendPayment({ toAddress: RECIPIENT, amount: "0.001" }), (res) => !res.success && res.error.code === "WRITE_TOOLS_DISABLED");
|
|
175
|
+
await record("approve_token_guard", () => handleApproveToken({ token: "USDC", amount: "max" }), (res) => !res.success && res.error.code === "WRITE_TOOLS_DISABLED");
|
|
176
|
+
await record("publish_risk_score_guard", () => handlePublishRiskScore({ action: "transfer", amount: "0.001", toAddress: RECIPIENT }), (res) => !res.success && res.error.code === "WRITE_TOOLS_DISABLED");
|
|
177
|
+
await record("x402_ssrf_block", () => handleX402PayAndFetch({ url: "http://127.0.0.1:4021/supported" }), (res) => !res.success && res.error.code === "SSRF_BLOCKED");
|
|
178
|
+
await record("x402_free_local_allowed", async () => withLocalServer(async (url) => {
|
|
179
|
+
const previous = process.env.ALLOW_LOCAL_X402_FETCH;
|
|
180
|
+
process.env.ALLOW_LOCAL_X402_FETCH = "true";
|
|
181
|
+
try {
|
|
182
|
+
return await handleX402PayAndFetch({ url });
|
|
217
183
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
184
|
+
finally {
|
|
185
|
+
if (previous === undefined)
|
|
186
|
+
delete process.env.ALLOW_LOCAL_X402_FETCH;
|
|
187
|
+
else
|
|
188
|
+
process.env.ALLOW_LOCAL_X402_FETCH = previous;
|
|
221
189
|
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
190
|
+
}), (res) => res.success === true && res.data.paymentExecuted === false);
|
|
191
|
+
await record("safehands_preflight_allow", () => handleSafeHandsPreflightCheck({ actionType: "send_payment", amount: "0.001", recipient: RECIPIENT, chainId: 688689, isMainnet: false }), (res) => res.success && res.data.decision !== "BLOCK");
|
|
192
|
+
await record("safehands_preflight_block_mainnet", () => handleSafeHandsPreflightCheck({ actionType: "send_payment", amount: "0.001", recipient: RECIPIENT, chainId: 1, isMainnet: true }), (res) => res.success && res.data.decision === "BLOCK" && res.data.riskLevel !== "LOW");
|
|
193
|
+
await record("safehands_preflight_block_unlimited_approval", () => handleSafeHandsPreflightCheck({ actionType: "approve_token", approvalAmount: "max", spender: RECIPIENT }), (res) => res.success && res.data.decision === "BLOCK");
|
|
194
|
+
await record("token_registry_status_canonical_usdc", () => handleTokenRegistryStatus({ token: USDC_ADDRESS }), (res) => res.success && res.data.status === "SKILL_ENGINE_CANONICAL_TOKEN" && res.data.verificationStatus === "DOCS_VERIFIED_FROM_PHAROS_SKILL_ENGINE");
|
|
195
|
+
await record("token_registry_status_custom_token", () => handleTokenRegistryStatus({ token: RECIPIENT }), (res) => res.success && res.data.status === "CUSTOM_NON_REGISTRY");
|
|
196
|
+
await record("token_registry_status_invalid", () => handleTokenRegistryStatus({ token: "not-an-address" }), (res) => res.success && res.data.status === "INVALID_ADDRESS");
|
|
197
|
+
await record("safehands_x402_preflight_ssrf", () => handleSafeHandsX402Preflight({ url: "http://127.0.0.1:4021/supported" }), (res) => !res.success && res.error.code === "SSRF_BLOCKED");
|
|
198
|
+
await record("safehands_wallet_health_no_wallet", () => handleSafeHandsWalletHealth({}), (res) => res.success && ["NOT_READY", "DEGRADED"].includes(String(res.data.status)));
|
|
199
|
+
await record("safehands_risk_report", () => handleSafeHandsRiskReport({ actionType: "approve_token", approvalAmount: "max", spender: RECIPIENT }), (res) => res.success && typeof res.data.summary === "string" && res.data.decision === "BLOCK");
|
|
200
|
+
await record("explain_risk", () => handleExplainRisk({ decision: "BLOCK", riskLevel: "HIGH", reasons: ["Unlimited approval requested"], requiredActions: ["Use limited approval"] }), (res) => res.success && String(res.data.explanation).includes("blocked"));
|
|
201
|
+
await record("managed_wallet_signer_deobfuscates", async () => {
|
|
202
|
+
const prevMode = process.env.WALLET_MODE;
|
|
203
|
+
const prevKey = process.env.WALLET_ENCRYPTION_KEY;
|
|
204
|
+
process.env.WALLET_MODE = "managed-testnet";
|
|
205
|
+
process.env.WALLET_ENCRYPTION_KEY = "test-only-key-for-smoke";
|
|
206
|
+
try {
|
|
207
|
+
const created = await handleCreateAgentWallet({ agentId: "smoke-agent", overwrite: true });
|
|
208
|
+
if (!created.success)
|
|
209
|
+
return created;
|
|
210
|
+
const signer = await getSigner("smoke-agent");
|
|
211
|
+
if (isSignerFailure(signer))
|
|
212
|
+
return fail(signer.error.code, signer.error.message, false, "managed_wallet_signer");
|
|
213
|
+
return ok({ address: signer.address, mode: signer.mode });
|
|
233
214
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
215
|
+
finally {
|
|
216
|
+
if (prevMode === undefined)
|
|
217
|
+
delete process.env.WALLET_MODE;
|
|
218
|
+
else
|
|
219
|
+
process.env.WALLET_MODE = prevMode;
|
|
220
|
+
if (prevKey === undefined)
|
|
221
|
+
delete process.env.WALLET_ENCRYPTION_KEY;
|
|
222
|
+
else
|
|
223
|
+
process.env.WALLET_ENCRYPTION_KEY = prevKey;
|
|
237
224
|
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
ok("check_token_security", `score=${r.securityProfile.safetyScore}, honeypot=${r.securityProfile.isHoneypot}`);
|
|
225
|
+
}, (res) => res.success && res.data.mode === "managed-testnet");
|
|
226
|
+
await record("private_key_never_returned_in_wallet_response", async () => {
|
|
227
|
+
const prevMode = process.env.WALLET_MODE;
|
|
228
|
+
const prevKey = process.env.WALLET_ENCRYPTION_KEY;
|
|
229
|
+
process.env.WALLET_MODE = "managed-testnet";
|
|
230
|
+
process.env.WALLET_ENCRYPTION_KEY = "test-only-key-for-smoke";
|
|
231
|
+
try {
|
|
232
|
+
const created = await handleCreateAgentWallet({ agentId: "smoke-agent-no-key", overwrite: true });
|
|
233
|
+
const wallet = await handleGetAgentWallet({ agentId: "smoke-agent-no-key" });
|
|
234
|
+
return ok({ created, wallet, serialized: JSON.stringify({ created, wallet }) });
|
|
249
235
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
236
|
+
finally {
|
|
237
|
+
if (prevMode === undefined)
|
|
238
|
+
delete process.env.WALLET_MODE;
|
|
239
|
+
else
|
|
240
|
+
process.env.WALLET_MODE = prevMode;
|
|
241
|
+
if (prevKey === undefined)
|
|
242
|
+
delete process.env.WALLET_ENCRYPTION_KEY;
|
|
243
|
+
else
|
|
244
|
+
process.env.WALLET_ENCRYPTION_KEY = prevKey;
|
|
253
245
|
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const evmSigner = toFacilitatorEvmSigner({
|
|
278
|
-
address: account.address,
|
|
279
|
-
getCode: (args) => walletClient.getCode(args),
|
|
280
|
-
readContract: (args) => walletClient.readContract({ ...args, args: args.args || [] }),
|
|
281
|
-
verifyTypedData: (args) => walletClient.verifyTypedData(args),
|
|
282
|
-
writeContract: (args) => walletClient.writeContract({ ...args, args: args.args || [] }),
|
|
283
|
-
sendTransaction: (args) => walletClient.sendTransaction(args),
|
|
284
|
-
waitForTransactionReceipt: (args) => walletClient.waitForTransactionReceipt(args),
|
|
285
|
-
});
|
|
286
|
-
const testFacilitator = new x402Facilitator();
|
|
287
|
-
testFacilitator.register("eip155:688689", new FacilitatorExactEvmScheme(evmSigner, {}));
|
|
288
|
-
const testFacilitatorClient = new HTTPFacilitatorClient({ url: `http://localhost:${testPort}` });
|
|
289
|
-
const testResourceServer = new x402ResourceServer(testFacilitatorClient);
|
|
290
|
-
const testEvmScheme = new ServerExactEvmScheme();
|
|
291
|
-
testEvmScheme.registerMoneyParser(async (amount, network) => {
|
|
292
|
-
if (network === "eip155:688689") {
|
|
293
|
-
return {
|
|
294
|
-
amount: Math.round(amount * 1e6).toString(),
|
|
295
|
-
asset: "0xcfC8330f4BCAB529c625D12781b1C19466A9Fc8B",
|
|
296
|
-
extra: { token: "USDC", name: "USDC", version: "2" },
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
return null;
|
|
300
|
-
});
|
|
301
|
-
testResourceServer.register("eip155:688689", testEvmScheme);
|
|
302
|
-
testApp.post("/verify", async (req, res) => {
|
|
303
|
-
try {
|
|
304
|
-
const { paymentPayload, paymentRequirements } = req.body;
|
|
305
|
-
const result = await testFacilitator.verify(paymentPayload, paymentRequirements);
|
|
306
|
-
res.json(result);
|
|
246
|
+
}, (res) => res.success && !String(res.data.serialized).toLowerCase().includes("privatekey") && !String(res.data.serialized).toLowerCase().includes("encryptedkey"));
|
|
247
|
+
await record("write_tools_use_signer_provider", async () => {
|
|
248
|
+
const offenders = readAllToolSourceFiles()
|
|
249
|
+
.filter((f) => /process\.env\.PRIVATE_KEY/.test(f.content) && !f.path.endsWith("x402Server.ts"))
|
|
250
|
+
.map((f) => f.path);
|
|
251
|
+
return offenders.length ? fail("PRIVATE_KEY_DIRECT_USAGE", offenders.join(", "), false, "source_scan") : ok({ offenders });
|
|
252
|
+
}, (res) => res.success);
|
|
253
|
+
await record("no_private_key_usage_outside_signer_provider", async () => {
|
|
254
|
+
const root = join(process.cwd(), "src");
|
|
255
|
+
const offenders = [];
|
|
256
|
+
function walk(dir) {
|
|
257
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
258
|
+
const full = join(dir, entry.name);
|
|
259
|
+
if (entry.isDirectory())
|
|
260
|
+
walk(full);
|
|
261
|
+
else if (entry.isFile() && entry.name.endsWith(".ts")) {
|
|
262
|
+
const rel = full.replace(process.cwd(), "").replace(/^[\\/]/, "").replace(/\\/g, "/");
|
|
263
|
+
if (rel.startsWith("src/lib/signer/"))
|
|
264
|
+
continue;
|
|
265
|
+
const content = readFileSync(full, "utf8");
|
|
266
|
+
if (/process\.env\.PRIVATE_KEY/.test(content))
|
|
267
|
+
offenders.push(rel);
|
|
268
|
+
}
|
|
307
269
|
}
|
|
308
|
-
catch (e) {
|
|
309
|
-
res.status(500).json({ error: e.message });
|
|
310
|
-
}
|
|
311
|
-
});
|
|
312
|
-
testApp.post("/settle", async (req, res) => {
|
|
313
|
-
try {
|
|
314
|
-
const { paymentPayload, paymentRequirements } = req.body;
|
|
315
|
-
const result = await testFacilitator.settle(paymentPayload, paymentRequirements);
|
|
316
|
-
res.json(result);
|
|
317
|
-
}
|
|
318
|
-
catch (e) {
|
|
319
|
-
res.status(500).json({ error: e.message });
|
|
320
|
-
}
|
|
321
|
-
});
|
|
322
|
-
testApp.get("/supported", (req, res) => {
|
|
323
|
-
res.json(testFacilitator.getSupported());
|
|
324
|
-
});
|
|
325
|
-
testApp.use(paymentMiddleware({
|
|
326
|
-
"GET /test-paid": {
|
|
327
|
-
accepts: {
|
|
328
|
-
scheme: "exact",
|
|
329
|
-
price: "0.001",
|
|
330
|
-
network: "eip155:688689",
|
|
331
|
-
payTo: WALLET,
|
|
332
|
-
},
|
|
333
|
-
description: "Test paid endpoint",
|
|
334
|
-
mimeType: "application/json",
|
|
335
|
-
},
|
|
336
|
-
}, testResourceServer));
|
|
337
|
-
testApp.get("/test-paid", (req, res) => {
|
|
338
|
-
res.json({ secret: "safehands-x402-passphrase" });
|
|
339
|
-
});
|
|
340
|
-
const serverListener = testApp.listen(testPort);
|
|
341
|
-
const r = await handleX402PayAndFetch({
|
|
342
|
-
url: `http://localhost:${testPort}/test-paid`,
|
|
343
|
-
method: "GET",
|
|
344
|
-
});
|
|
345
|
-
serverListener.close();
|
|
346
|
-
if (r.success && r.data && r.data.secret === "safehands-x402-passphrase") {
|
|
347
|
-
console.log(`17. x402_pay_and_fetch → ✅ success, payment=${r.paymentExecuted}`);
|
|
348
|
-
ok("x402_pay_and_fetch", `paymentExecuted=${r.paymentExecuted}`);
|
|
349
270
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
271
|
+
walk(root);
|
|
272
|
+
return offenders.length ? fail("PRIVATE_KEY_DIRECT_USAGE", offenders.join(", "), false, "source_scan") : ok({ offenders });
|
|
273
|
+
}, (res) => res.success);
|
|
274
|
+
await record("skill_cli_preflight_valid_json", async () => runBuiltCli(["skill", "safehands_preflight_check", "--input-json", JSON.stringify({ actionType: "send_payment", chainId: 688689, isMainnet: false, amount: "0.001", recipient: RECIPIENT })]), (res) => res.success && res.data.decision !== undefined && res.data.chainId === 688689);
|
|
275
|
+
await record("skill_cli_invalid_json_structured_error", async () => runBuiltCli(["skill", "safehands_preflight_check", "--input-json", "{not-json"]), (res) => !res.success && res.error.code === "INVALID_INPUT_JSON" && res.error.source === "safehands_cli");
|
|
276
|
+
await record("skill_cli_readonly_without_private_key", async () => runBuiltCli(["skill", "token_registry_status", "--input-json", JSON.stringify({ token: USDC_ADDRESS })]), (res) => res.success && res.data.status === "SKILL_ENGINE_CANONICAL_TOKEN");
|
|
277
|
+
await record("skill_engine_example_files_exist", async () => {
|
|
278
|
+
const files = [
|
|
279
|
+
"examples/pharos-skill-engine/SKILL.safehands.md",
|
|
280
|
+
"examples/pharos-skill-engine/references/safehands.md",
|
|
281
|
+
"examples/pharos-skill-engine/assets/safehands/policy-defaults.json",
|
|
282
|
+
"examples/pharos-skill-engine/assets/safehands/example-actions.json",
|
|
283
|
+
];
|
|
284
|
+
const missing = files.filter((file) => !existsSync(join(process.cwd(), file)));
|
|
285
|
+
return missing.length ? fail("SKILL_ENGINE_FILES_MISSING", missing.join(", "), false, "skill_engine_examples") : ok({ files });
|
|
286
|
+
}, (res) => res.success);
|
|
287
|
+
await record("skill_engine_reference_required_sections", async () => {
|
|
288
|
+
const text = readFileSync(join(process.cwd(), "examples/pharos-skill-engine/references/safehands.md"), "utf8");
|
|
289
|
+
const required = [
|
|
290
|
+
"## Overview",
|
|
291
|
+
"## Command Template",
|
|
292
|
+
"## SafeHands Preflight Check",
|
|
293
|
+
"## SafeHands x402 Preflight",
|
|
294
|
+
"## SafeHands Wallet Health",
|
|
295
|
+
"## Token Registry Status",
|
|
296
|
+
"## Explain Risk",
|
|
297
|
+
"## SafeHands Risk Report",
|
|
298
|
+
"### Error Handling",
|
|
299
|
+
"### Agent Guidelines",
|
|
300
|
+
];
|
|
301
|
+
return hasAll(text, required) ? ok({ required }) : fail("SKILL_ENGINE_REFERENCE_INCOMPLETE", "Missing one or more required reference sections.", false, "skill_engine_examples");
|
|
302
|
+
}, (res) => res.success);
|
|
303
|
+
await record("skill_engine_skill_capability_rows", async () => {
|
|
304
|
+
const text = readFileSync(join(process.cwd(), "examples/pharos-skill-engine/SKILL.safehands.md"), "utf8");
|
|
305
|
+
const required = [
|
|
306
|
+
"SafeHands Preflight Check",
|
|
307
|
+
"SafeHands x402 Preflight",
|
|
308
|
+
"SafeHands Wallet Health",
|
|
309
|
+
"Token Registry Status",
|
|
310
|
+
"Explain Risk",
|
|
311
|
+
"SafeHands Risk Report",
|
|
312
|
+
"references/safehands.md#safehands-preflight-check",
|
|
313
|
+
];
|
|
314
|
+
return hasAll(text, required) ? ok({ required }) : fail("SKILL_ENGINE_CAPABILITY_ROWS_MISSING", "SKILL.safehands.md is missing required capability rows.", false, "skill_engine_examples");
|
|
315
|
+
}, (res) => res.success);
|
|
316
|
+
// ── New: skill/ package structure tests ─────────────────────────────
|
|
317
|
+
await record("skill_package_skill_md_exists", async () => {
|
|
318
|
+
const path = join(process.cwd(), "skill/SKILL.md");
|
|
319
|
+
return existsSync(path) ? ok({ file: "skill/SKILL.md" }) : fail("SKILL_PACKAGE_MISSING", "skill/SKILL.md not found", false, "skill_package");
|
|
320
|
+
}, (res) => res.success);
|
|
321
|
+
await record("skill_package_yaml_frontmatter", async () => {
|
|
322
|
+
const text = readFileSync(join(process.cwd(), "skill/SKILL.md"), "utf8");
|
|
323
|
+
const hasYaml = text.startsWith("---") && text.includes("name: safehands-pharos-guard");
|
|
324
|
+
return hasYaml ? ok({ frontmatter: true }) : fail("SKILL_PACKAGE_NO_FRONTMATTER", "YAML frontmatter missing or name mismatch", false, "skill_package");
|
|
325
|
+
}, (res) => res.success);
|
|
326
|
+
await record("skill_package_references_exist", async () => {
|
|
327
|
+
const path = join(process.cwd(), "skill/references/safehands.md");
|
|
328
|
+
return existsSync(path) ? ok({ file: "skill/references/safehands.md" }) : fail("SKILL_PACKAGE_REF_MISSING", "skill/references/safehands.md not found", false, "skill_package");
|
|
329
|
+
}, (res) => res.success);
|
|
330
|
+
await record("skill_package_assets_exist", async () => {
|
|
331
|
+
const files = [
|
|
332
|
+
"skill/assets/safehands/policy-defaults.json",
|
|
333
|
+
"skill/assets/safehands/example-actions.json",
|
|
334
|
+
];
|
|
335
|
+
const missing = files.filter((f) => !existsSync(join(process.cwd(), f)));
|
|
336
|
+
return missing.length === 0 ? ok({ files }) : fail("SKILL_PACKAGE_ASSETS_MISSING", missing.join(", "), false, "skill_package");
|
|
337
|
+
}, (res) => res.success);
|
|
338
|
+
await record("skill_package_example_uses_skill_engine_usdc", async () => {
|
|
339
|
+
const text = readFileSync(join(process.cwd(), "skill/assets/safehands/example-actions.json"), "utf8");
|
|
340
|
+
const usesSkillEngineUsdc = text.includes("0xE0BE08c77f415F577A1B3A9aD7a1Df1479564ec8");
|
|
341
|
+
const usesCircleUsdc = text.includes("0xcfC8330f4BCAB529c625D12781b1C19466A9Fc8B");
|
|
342
|
+
return usesSkillEngineUsdc && !usesCircleUsdc
|
|
343
|
+
? ok({ correctUsdc: true })
|
|
344
|
+
: fail("SKILL_EXAMPLE_WRONG_USDC", "example-actions.json should use Skill Engine USDC (0xE0BE...), not Circle USDC (0xcfC8...)", false, "skill_package");
|
|
345
|
+
}, (res) => res.success);
|
|
346
|
+
await record("token_registry_circle_usdc_alternate", () => handleTokenRegistryStatus({ token: "0xcfC8330f4BCAB529c625D12781b1C19466A9Fc8B" }), (res) => res.success && res.data.status === "ALTERNATE_SOURCE_TOKEN" && res.data.verificationStatus === "CIRCLE_REFERENCED_USDC");
|
|
347
|
+
await record("cli_help_works", async () => {
|
|
348
|
+
const raw = runBuiltCliRaw(["--help"]);
|
|
349
|
+
return raw.status === 0 && raw.stdout.includes("Transaction Safety Firewall") && raw.stdout.includes("safehands_preflight_check")
|
|
350
|
+
? ok({ stdoutBytes: raw.stdout.length })
|
|
351
|
+
: fail("CLI_HELP_FAILED", `status=${raw.status} stdout=${raw.stdout.slice(0, 200)} stderr=${raw.stderr.slice(0, 200)}`, false, "cli_help");
|
|
352
|
+
}, (res) => res.success);
|
|
353
|
+
await record("demo_runs_or_fails_gracefully", async () => {
|
|
354
|
+
const raw = runBuiltCliRaw(["--demo"]);
|
|
355
|
+
return raw.status === 0 && raw.stdout.includes("Demo Complete") && raw.stdout.includes("SSRF_BLOCKED")
|
|
356
|
+
? ok({ stdoutBytes: raw.stdout.length })
|
|
357
|
+
: fail("DEMO_FAILED", `status=${raw.status} stdout=${raw.stdout.slice(0, 300)} stderr=${raw.stderr.slice(0, 300)}`, false, "demo");
|
|
358
|
+
}, (res) => res.success);
|
|
359
|
+
await record("readme_contains_final_positioning", async () => {
|
|
360
|
+
const text = readFileSync(join(process.cwd(), "README.md"), "utf8");
|
|
361
|
+
const required = [
|
|
362
|
+
"# SafeHands-Pharos: Transaction Safety Firewall for AI Agents",
|
|
363
|
+
"Pharos Skill Engine-compatible MCP package",
|
|
364
|
+
"SafeHands is a guardrail layer",
|
|
365
|
+
"WRITE_TOOLS_ENABLED=false",
|
|
366
|
+
"Testnet scope",
|
|
367
|
+
"Using SafeHands with Pharos Skill Engine",
|
|
368
|
+
];
|
|
369
|
+
return hasAll(text, required) ? ok({ required }) : fail("README_POSITIONING_INCOMPLETE", "Missing final positioning text.", false, "readme");
|
|
370
|
+
}, (res) => res.success);
|
|
371
|
+
await record("env_example_safe_placeholders", async () => {
|
|
372
|
+
const text = readFileSync(join(process.cwd(), ".env.example"), "utf8");
|
|
373
|
+
const required = ["WALLET_MODE=none", "WRITE_TOOLS_ENABLED=false", "PRIVATE_KEY=", "MAX_X402_PAYMENT_USDC=0.01"];
|
|
374
|
+
const hasFakeSecret = /PRIVATE_KEY=0x[0-9a-fA-F]{16,}/.test(text) || /WALLET_ENCRYPTION_KEY=.+\S/.test(text);
|
|
375
|
+
return hasAll(text, required) && !hasFakeSecret ? ok({ required }) : fail("ENV_EXAMPLE_UNSAFE", "Missing safe defaults or contains secret-looking placeholders.", false, "env_example");
|
|
376
|
+
}, (res) => res.success);
|
|
377
|
+
await record("npm_pack_excludes_secrets", async () => runNpmPackDryRun(), (res) => res.success && res.data.unsafe.length === 0);
|
|
378
|
+
console.log("\n# Status Tool Note");
|
|
379
|
+
console.log("─".repeat(100));
|
|
380
|
+
rows.forEach((r, i) => {
|
|
381
|
+
console.log(`${String(i + 1).padStart(2, "0")} ${r.status.padEnd(6)} ${r.tool.padEnd(28)} ${r.note}`);
|
|
379
382
|
});
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
383
|
+
const failed = rows.filter((r) => r.status === "FAIL");
|
|
384
|
+
console.log("─".repeat(100));
|
|
385
|
+
console.log(`${rows.length - failed.length}/${rows.length} smoke checks passed.`);
|
|
386
|
+
if (failed.length > 0) {
|
|
387
|
+
console.error("\nFailed smoke checks:");
|
|
388
|
+
for (const f of failed)
|
|
389
|
+
console.error(`- ${f.tool}: ${f.note}`);
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
|
387
392
|
}
|
|
388
|
-
run().catch(
|
|
393
|
+
run().catch((err) => {
|
|
394
|
+
const structured = fail("SMOKE_TEST_FAILED", err instanceof Error ? err.message : String(err), false, "testTools");
|
|
395
|
+
console.error(JSON.stringify(structured, null, 2));
|
|
396
|
+
process.exit(1);
|
|
397
|
+
});
|
|
389
398
|
//# sourceMappingURL=testTools.js.map
|