safehands-pharos 1.3.0 → 1.4.0
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 +64 -26
- package/README.md +333 -445
- package/dist/cli.d.ts +5 -5
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +124 -98
- package/dist/cli.js.map +1 -1
- package/dist/demo.d.ts +1 -1
- package/dist/demo.js +171 -171
- package/dist/index.d.ts +2 -2
- package/dist/index.js +138 -85
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +1 -1
- package/dist/init.js +65 -65
- package/dist/lib/auditLog.d.ts +9 -0
- package/dist/lib/auditLog.d.ts.map +1 -0
- package/dist/lib/auditLog.js +30 -0
- package/dist/lib/auditLog.js.map +1 -0
- package/dist/lib/constants.d.ts +291 -291
- package/dist/lib/constants.js +292 -292
- package/dist/lib/dodoApi.d.ts +78 -70
- package/dist/lib/dodoApi.d.ts.map +1 -1
- package/dist/lib/dodoApi.js +196 -178
- package/dist/lib/dodoApi.js.map +1 -1
- package/dist/lib/http.d.ts +14 -14
- package/dist/lib/http.js +118 -118
- package/dist/lib/pharosClient.d.ts +58 -58
- package/dist/lib/pharosClient.d.ts.map +1 -1
- package/dist/lib/pharosClient.js +63 -53
- package/dist/lib/pharosClient.js.map +1 -1
- package/dist/lib/policy/actionPolicyEngine.d.ts +53 -53
- package/dist/lib/policy/actionPolicyEngine.js +212 -212
- package/dist/lib/riskEngine.d.ts +26 -26
- package/dist/lib/riskEngine.js +283 -283
- package/dist/lib/signer/index.d.ts +24 -24
- package/dist/lib/signer/index.d.ts.map +1 -1
- package/dist/lib/signer/index.js +88 -89
- package/dist/lib/signer/index.js.map +1 -1
- package/dist/lib/spendAccumulator.d.ts +10 -0
- package/dist/lib/spendAccumulator.d.ts.map +1 -0
- package/dist/lib/spendAccumulator.js +54 -0
- package/dist/lib/spendAccumulator.js.map +1 -0
- package/dist/lib/testDodoLive.d.ts +1 -1
- package/dist/lib/testDodoLive.js +104 -104
- package/dist/lib/testLiveSafehands.d.ts +1 -1
- package/dist/lib/testLiveSafehands.js +92 -92
- package/dist/lib/testRpc.d.ts +1 -1
- package/dist/lib/testRpc.js +29 -29
- package/dist/lib/testRpcLive.d.ts +1 -1
- package/dist/lib/testRpcLive.js +88 -88
- package/dist/lib/testTools.d.ts +1 -1
- package/dist/lib/testTools.js +397 -397
- package/dist/lib/testX402Live.d.ts +1 -1
- package/dist/lib/testX402Live.js +159 -159
- package/dist/lib/toolResponse.d.ts +25 -25
- package/dist/lib/toolResponse.js +53 -53
- package/dist/lib/wallet/index.d.ts +37 -18
- package/dist/lib/wallet/index.d.ts.map +1 -1
- package/dist/lib/wallet/index.js +128 -70
- package/dist/lib/wallet/index.js.map +1 -1
- package/dist/scripts/checkDeploy.d.ts +1 -1
- package/dist/scripts/checkDeploy.js +24 -24
- package/dist/scripts/deployRegistry.d.ts +1 -1
- package/dist/scripts/deployRegistry.js +100 -100
- package/dist/scripts/testRegistry.d.ts +1 -1
- package/dist/scripts/testRegistry.js +43 -43
- package/dist/tools/approveToken.d.ts +45 -46
- package/dist/tools/approveToken.d.ts.map +1 -1
- package/dist/tools/approveToken.js +85 -83
- package/dist/tools/approveToken.js.map +1 -1
- package/dist/tools/assessRisk.d.ts +79 -79
- package/dist/tools/assessRisk.d.ts.map +1 -1
- package/dist/tools/assessRisk.js +104 -93
- package/dist/tools/assessRisk.js.map +1 -1
- package/dist/tools/checkAllowance.d.ts +43 -36
- package/dist/tools/checkAllowance.d.ts.map +1 -1
- package/dist/tools/checkAllowance.js +56 -42
- package/dist/tools/checkAllowance.js.map +1 -1
- package/dist/tools/checkTokenSecurity.d.ts +46 -46
- package/dist/tools/checkTokenSecurity.d.ts.map +1 -1
- package/dist/tools/checkTokenSecurity.js +95 -88
- package/dist/tools/checkTokenSecurity.js.map +1 -1
- package/dist/tools/createAgentWallet.d.ts +26 -26
- package/dist/tools/createAgentWallet.d.ts.map +1 -1
- package/dist/tools/createAgentWallet.js +58 -59
- package/dist/tools/createAgentWallet.js.map +1 -1
- package/dist/tools/estimateGas.d.ts +79 -79
- package/dist/tools/estimateGas.js +124 -124
- package/dist/tools/executeSwap.d.ts +61 -59
- package/dist/tools/executeSwap.d.ts.map +1 -1
- package/dist/tools/executeSwap.js +141 -129
- package/dist/tools/executeSwap.js.map +1 -1
- package/dist/tools/explainRisk.d.ts +29 -29
- package/dist/tools/explainRisk.js +32 -32
- package/dist/tools/getAgentWallet.d.ts +21 -21
- package/dist/tools/getAgentWallet.js +27 -27
- package/dist/tools/getAgentWalletBalance.d.ts +11 -11
- package/dist/tools/getAgentWalletBalance.js +70 -70
- package/dist/tools/getExecutionHistory.d.ts +49 -51
- package/dist/tools/getExecutionHistory.d.ts.map +1 -1
- package/dist/tools/getExecutionHistory.js +154 -93
- package/dist/tools/getExecutionHistory.js.map +1 -1
- package/dist/tools/getGasPrice.d.ts +43 -43
- package/dist/tools/getGasPrice.js +59 -59
- package/dist/tools/getPoolInfo.d.ts +75 -75
- package/dist/tools/getPoolInfo.js +137 -137
- package/dist/tools/getTokenPrice.d.ts +113 -113
- package/dist/tools/getTokenPrice.js +117 -117
- package/dist/tools/getTransactionStatus.d.ts +43 -57
- package/dist/tools/getTransactionStatus.d.ts.map +1 -1
- package/dist/tools/getTransactionStatus.js +59 -67
- package/dist/tools/getTransactionStatus.js.map +1 -1
- package/dist/tools/getWalletBalance.d.ts +68 -68
- package/dist/tools/getWalletBalance.js +87 -87
- package/dist/tools/publishRiskScore.d.ts +63 -63
- package/dist/tools/publishRiskScore.d.ts.map +1 -1
- package/dist/tools/publishRiskScore.js +88 -85
- package/dist/tools/publishRiskScore.js.map +1 -1
- package/dist/tools/queryRiskRegistry.d.ts +38 -48
- package/dist/tools/queryRiskRegistry.d.ts.map +1 -1
- package/dist/tools/queryRiskRegistry.js +55 -60
- package/dist/tools/queryRiskRegistry.js.map +1 -1
- package/dist/tools/safehandsPreflightCheck.d.ts +77 -77
- package/dist/tools/safehandsPreflightCheck.js +47 -47
- package/dist/tools/safehandsRiskReport.d.ts +81 -81
- package/dist/tools/safehandsRiskReport.js +28 -28
- package/dist/tools/safehandsSafeExecute.d.ts +20 -20
- package/dist/tools/safehandsSafeExecute.d.ts.map +1 -1
- package/dist/tools/safehandsSafeExecute.js +81 -75
- package/dist/tools/safehandsSafeExecute.js.map +1 -1
- package/dist/tools/safehandsWalletHealth.d.ts +14 -14
- package/dist/tools/safehandsWalletHealth.js +103 -103
- package/dist/tools/safehandsX402Preflight.d.ts +26 -26
- package/dist/tools/safehandsX402Preflight.js +65 -65
- package/dist/tools/sendPayment.d.ts +57 -58
- package/dist/tools/sendPayment.d.ts.map +1 -1
- package/dist/tools/sendPayment.js +117 -108
- package/dist/tools/sendPayment.js.map +1 -1
- package/dist/tools/simulateTransaction.d.ts +60 -81
- package/dist/tools/simulateTransaction.d.ts.map +1 -1
- package/dist/tools/simulateTransaction.js +83 -88
- package/dist/tools/simulateTransaction.js.map +1 -1
- package/dist/tools/tokenRegistryStatus.d.ts +26 -26
- package/dist/tools/tokenRegistryStatus.js +96 -96
- package/dist/tools/x402PayAndFetch.d.ts +81 -81
- package/dist/tools/x402PayAndFetch.d.ts.map +1 -1
- package/dist/tools/x402PayAndFetch.js +152 -149
- package/dist/tools/x402PayAndFetch.js.map +1 -1
- package/dist/x402Server.d.ts +1 -1
- package/dist/x402Server.js +252 -252
- package/examples/dashboard/index.html +337 -0
- package/package.json +83 -84
- package/.agents/skill/safehands/SKILL.md +0 -212
- package/.agents/skill/safehands/assets/networks.json +0 -24
- package/.agents/skill/safehands/assets/tokens.json +0 -66
- package/.agents/wallets.json +0 -20
- package/docs/reports/OFFICIAL_DOCS_ALIGNMENT_REPORT.md +0 -137
- package/docs/reports/final_audit_report.md +0 -307
- package/docs/reports/live_verification_report.md +0 -147
- package/docs/reports/pharos_skill_engine_alignment_report.md +0 -85
package/dist/lib/testTools.js
CHANGED
|
@@ -1,398 +1,398 @@
|
|
|
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
|
-
// ────────────────────────────────────────────────────────────────────────
|
|
6
|
-
import express from "express";
|
|
7
|
-
import { handleAssessRisk } from "../tools/assessRisk.js";
|
|
8
|
-
import { handleExecuteSwap } from "../tools/executeSwap.js";
|
|
9
|
-
import { handleSendPayment } from "../tools/sendPayment.js";
|
|
10
|
-
import { handleSimulateTransaction } from "../tools/simulateTransaction.js";
|
|
11
|
-
import { handleGetTokenPrice } from "../tools/getTokenPrice.js";
|
|
12
|
-
import { handleGetWalletBalance } from "../tools/getWalletBalance.js";
|
|
13
|
-
import { handleEstimateGas } from "../tools/estimateGas.js";
|
|
14
|
-
import { handlePublishRiskScore } from "../tools/publishRiskScore.js";
|
|
15
|
-
import { handleApproveToken } from "../tools/approveToken.js";
|
|
16
|
-
import { handleCheckTokenSecurity } from "../tools/checkTokenSecurity.js";
|
|
17
|
-
import { handleX402PayAndFetch } from "../tools/x402PayAndFetch.js";
|
|
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;
|
|
38
|
-
}
|
|
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
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
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();
|
|
71
|
-
try {
|
|
72
|
-
return JSON.parse(stdout);
|
|
73
|
-
}
|
|
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");
|
|
76
|
-
}
|
|
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");
|
|
99
|
-
try {
|
|
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");
|
|
113
|
-
}
|
|
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
|
-
});
|
|
141
|
-
}
|
|
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
|
-
});
|
|
151
|
-
try {
|
|
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`);
|
|
156
|
-
}
|
|
157
|
-
finally {
|
|
158
|
-
if ("closeAllConnections" in server)
|
|
159
|
-
server.closeAllConnections();
|
|
160
|
-
await new Promise((resolve, reject) => server.close((err) => (err ? reject(err) : resolve())));
|
|
161
|
-
}
|
|
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 });
|
|
183
|
-
}
|
|
184
|
-
finally {
|
|
185
|
-
if (previous === undefined)
|
|
186
|
-
delete process.env.ALLOW_LOCAL_X402_FETCH;
|
|
187
|
-
else
|
|
188
|
-
process.env.ALLOW_LOCAL_X402_FETCH = previous;
|
|
189
|
-
}
|
|
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 });
|
|
214
|
-
}
|
|
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;
|
|
224
|
-
}
|
|
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 }) });
|
|
235
|
-
}
|
|
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;
|
|
245
|
-
}
|
|
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
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
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}`);
|
|
382
|
-
});
|
|
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
|
-
}
|
|
392
|
-
}
|
|
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
|
-
});
|
|
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
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
6
|
+
import express from "express";
|
|
7
|
+
import { handleAssessRisk } from "../tools/assessRisk.js";
|
|
8
|
+
import { handleExecuteSwap } from "../tools/executeSwap.js";
|
|
9
|
+
import { handleSendPayment } from "../tools/sendPayment.js";
|
|
10
|
+
import { handleSimulateTransaction } from "../tools/simulateTransaction.js";
|
|
11
|
+
import { handleGetTokenPrice } from "../tools/getTokenPrice.js";
|
|
12
|
+
import { handleGetWalletBalance } from "../tools/getWalletBalance.js";
|
|
13
|
+
import { handleEstimateGas } from "../tools/estimateGas.js";
|
|
14
|
+
import { handlePublishRiskScore } from "../tools/publishRiskScore.js";
|
|
15
|
+
import { handleApproveToken } from "../tools/approveToken.js";
|
|
16
|
+
import { handleCheckTokenSecurity } from "../tools/checkTokenSecurity.js";
|
|
17
|
+
import { handleX402PayAndFetch } from "../tools/x402PayAndFetch.js";
|
|
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;
|
|
38
|
+
}
|
|
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
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
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();
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(stdout);
|
|
73
|
+
}
|
|
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");
|
|
76
|
+
}
|
|
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");
|
|
99
|
+
try {
|
|
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");
|
|
113
|
+
}
|
|
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
|
+
});
|
|
141
|
+
}
|
|
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
|
+
});
|
|
151
|
+
try {
|
|
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`);
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
if ("closeAllConnections" in server)
|
|
159
|
+
server.closeAllConnections();
|
|
160
|
+
await new Promise((resolve, reject) => server.close((err) => (err ? reject(err) : resolve())));
|
|
161
|
+
}
|
|
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 });
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
if (previous === undefined)
|
|
186
|
+
delete process.env.ALLOW_LOCAL_X402_FETCH;
|
|
187
|
+
else
|
|
188
|
+
process.env.ALLOW_LOCAL_X402_FETCH = previous;
|
|
189
|
+
}
|
|
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 });
|
|
214
|
+
}
|
|
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;
|
|
224
|
+
}
|
|
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 }) });
|
|
235
|
+
}
|
|
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;
|
|
245
|
+
}
|
|
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
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
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}`);
|
|
382
|
+
});
|
|
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
|
+
}
|
|
392
|
+
}
|
|
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
|
+
});
|
|
398
398
|
//# sourceMappingURL=testTools.js.map
|