shll-skills 1.3.0 → 2.0.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/README.md CHANGED
@@ -13,13 +13,17 @@ npm install -g shll-skills
13
13
  ## Quick Start
14
14
 
15
15
  ```bash
16
- export RUNNER_PRIVATE_KEY="0x..."
16
+ # 1. Generate an operator wallet (hot wallet for AI)
17
+ shll-run generate-wallet
18
+ # -> Outputs address + private key
17
19
 
18
- # 1. One-click onboarding: rent agent + authorize + fund vault
19
- shll-run init --listing-id 0xABC...DEF --days 30 --fund 0.5
20
- # -> Agent #5 is ready!
20
+ export RUNNER_PRIVATE_KEY="0x...(operator key)..."
21
21
 
22
- # 2. Trade
22
+ # 2. Get setup instructions (user completes on shll.run with their OWN wallet)
23
+ shll-run setup-guide --listing-id 0xABC...DEF --days 30
24
+ # -> Outputs shll.run link for rent + authorize + fund
25
+
26
+ # 3. After setup, trade with your token-id
23
27
  shll-run swap --from BNB --to USDC --amount 0.1 --token-id 5
24
28
  ```
25
29
 
@@ -77,18 +81,29 @@ AI Agent -> CLI command -> PolicyClient.validate() -> PolicyGuard (on-chain) ->
77
81
  3. If approved, `AgentNFA.execute()` routes through PolicyGuard -> vault
78
82
  4. PolicyGuard enforces: spending limits, cooldowns, DEX whitelist, receiver guard
79
83
 
80
- ## Security
84
+ ## Security: Dual-Wallet Architecture
85
+
86
+ SHLL enforces **separation of owner and operator wallets**:
87
+
88
+ | | Owner Wallet | Operator Wallet (RUNNER_PRIVATE_KEY) |
89
+ |---|---|---|
90
+ | **Who holds it** | User (MetaMask/hardware) | AI agent |
91
+ | **Can trade** | — | ✅ Within PolicyGuard limits |
92
+ | **Can withdraw vault** | ✅ | ❌ |
93
+ | **Can transfer NFT** | ✅ | ❌ |
94
+ | **Risk if leaked** | 🚨 Full vault access | ⚠️ Limited to policy-allowed trades |
81
95
 
82
- - **On-chain enforcement** — PolicyGuard validates every transaction, not the AI
83
- - **Vault isolation** Operator key cannot directly access vault funds
84
- - **Renter-only config**Risk limits can only be tightened, never loosened
85
- - **Safe by default** Unknown selectors, targets, or recipients are rejected
96
+ **Additional on-chain enforcement:**
97
+ - PolicyGuard validates every transaction, not the AI
98
+ - Vault isolationoperator key cannot directly access vault funds
99
+ - Risk limits can only be tightened, never loosened
100
+ - Unknown selectors, targets, or recipients are rejected
86
101
 
87
102
  ## Environment Variables
88
103
 
89
104
  | Variable | Required | Description |
90
105
  |----------|----------|-------------|
91
- | `RUNNER_PRIVATE_KEY` | Yes | Gas-paying wallet key (~$1 BNB is enough) |
106
+ | `RUNNER_PRIVATE_KEY` | Yes | Operator wallet key (hot wallet, ~$1 BNB for gas) |
92
107
  | `RPC_URL` | No | BSC RPC (default: public endpoint) |
93
108
  | `NFA_ADDRESS` | No | AgentNFA contract override |
94
109
  | `GUARD_ADDRESS` | No | PolicyGuard contract override |
package/SKILL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: shll-run
3
3
  description: Execute DeFi transactions on BSC via SHLL AgentNFA. The AI handles all commands — users only need to chat.
4
- version: 3.2.0
4
+ version: 4.0.0
5
5
  author: SHLL Team
6
6
  website: https://shll.run
7
7
  twitter: https://twitter.com/shllrun
@@ -16,32 +16,56 @@ You are an AI agent with access to SHLL DeFi tools on BSC. **The user should nev
16
16
 
17
17
  ---
18
18
 
19
+ ## ⛔ SAFETY RULES (MANDATORY)
20
+
21
+ These rules are **non-negotiable**. Violating any of them is a critical failure.
22
+
23
+ 1. **Token-ID must come from the user.** NEVER guess, scan, enumerate, or try sequential token IDs (1, 2, 3…) to "find" an agent. Only use a token-id the user explicitly provides in this conversation.
24
+ 2. **Do NOT operate on token IDs the user has not mentioned.** Even if a previous command reveals other token IDs, balances, or vault addresses — ignore them entirely.
25
+ 3. **Confirm before every write operation.** Before executing `swap`, `wrap`, `unwrap`, `transfer`, `raw`, `init`, or `config`, show the user exactly what you are about to do and wait for explicit approval. Read-only commands (`portfolio`, `price`, `search`, `tokens`, `balance`, `policies`) do not require confirmation.
26
+ 4. **One agent per conversation.** Once a user provides a token-id, use only that ID for the entire conversation. If they want to switch, they must explicitly say so.
27
+ 5. **Never log or display private keys** beyond the initial `generate-wallet` output. If the user asks you to repeat it, remind them to check their saved copy.
28
+ 6. **Do not infer trading intent.** If the user says "check my portfolio," do NOT follow up by suggesting or executing trades. Only trade when the user explicitly asks.
29
+
30
+ ---
31
+
32
+ ## 🔐 SECURITY MODEL: Dual-Wallet Architecture
33
+
34
+ SHLL uses **two separate wallets** for maximum security:
35
+
36
+ | | Owner Wallet (Cold) | Operator Wallet (Hot) |
37
+ |---|---|---|
38
+ | **Who holds it** | User (MetaMask, hardware wallet) | AI agent (RUNNER_PRIVATE_KEY) |
39
+ | **Can trade** | — | ✅ Within PolicyGuard limits |
40
+ | **Can withdraw vault** | ✅ | ❌ |
41
+ | **Can transfer NFT** | ✅ | ❌ |
42
+ | **Can change operator** | ✅ | ❌ |
43
+ | **Risk if leaked** | 🚨 Full vault access | ⚠️ Limited to policy-allowed trades |
44
+
45
+ **Critical rule: You (the AI) ONLY have access to the operator wallet. You must NEVER ask for, store, or handle the user's owner wallet key. All owner-level operations (rent, fund, withdraw) are done by the user on https://shll.run.**
46
+
47
+ ---
48
+
19
49
  ## CONVERSATION FLOW: New User Onboarding
20
50
 
21
51
  When a user wants to use SHLL for the first time, follow this decision tree:
22
52
 
23
- ### Step 1: Check if they have a gas wallet
53
+ ### Step 1: Generate operator wallet
24
54
 
25
- Ask: *"Do you already have a wallet set up for SHLL? (a RUNNER_PRIVATE_KEY)"*
55
+ Ask: *"Do you already have an operator wallet set up for SHLL? (a RUNNER_PRIVATE_KEY)"*
26
56
 
27
- **If NO** -> Generate one for them:
57
+ **If NO** Generate one:
28
58
  ```bash
29
59
  shll-run generate-wallet
30
60
  ```
31
- This outputs:
32
- ```json
33
- {"status":"success","address":"0xABC...","privateKey":"0x123...","note":"..."}
34
- ```
35
61
 
36
62
  Tell the user:
37
- - *"I've generated a new wallet for you."*
63
+ - *"I've generated an operator wallet for you."*
38
64
  - *"Address: `0xABC...` — send about $1 of BNB here for gas fees."*
39
- - *"Private key: `0x123...` — save this somewhere safe, you'll need it."*
40
- - *"This wallet ONLY pays gas fees (~$0.01 per transaction). Your trading funds are stored in a separate secure vault on-chain that this key cannot access directly."*
41
-
42
- Then wait for them to fund it.
65
+ - *"Private key: `0x123...` — save this securely, then set it as RUNNER_PRIVATE_KEY."*
66
+ - *"This is an OPERATOR wallet it can only trade within safety limits. It CANNOT withdraw your vault funds or touch your agent ownership."*
43
67
 
44
- **If YES** -> Ask them to provide it, then set it:
68
+ **If YES** Set it:
45
69
  ```bash
46
70
  export RUNNER_PRIVATE_KEY="0x..."
47
71
  ```
@@ -53,26 +77,28 @@ shll-run balance
53
77
  ```
54
78
 
55
79
  If `sufficient: false`, tell the user:
56
- *"Your gas wallet needs more BNB. Please send at least $1 of BNB to `<address>`. You can buy BNB on Binance, OKX, or any exchange and withdraw to this address on BSC (BEP-20)."*
80
+ *"Your operator wallet needs more BNB for gas fees. Send at least $1 of BNB to `<address>`. You can buy BNB on Binance, OKX, or any exchange and withdraw to BSC (BEP-20)."*
57
81
 
58
- Wait until balance is sufficient before proceeding.
82
+ Wait until balance is sufficient.
59
83
 
60
- ### Step 3: Check if they have a token-id (Agent)
84
+ ### Step 3: Set up agent (user does this themselves)
61
85
 
62
86
  Ask: *"Do you already have a SHLL Agent? (a token-id number)"*
63
87
 
64
- **If NO** -> Create one:
88
+ **If NO** Guide them to set up via shll.run:
65
89
  ```bash
66
- shll-run init --listing-id <LISTING_ID> --days 30 --fund 0.1
90
+ shll-run setup-guide --listing-id <LISTING_ID> --days 30
67
91
  ```
68
92
 
69
- **About `listing-id`:** This is the SHLL Agent template ID from the marketplace. If the user doesn't know it, tell them:
70
- *"You can find available Agent templates at https://shll.run. Each template has a listing-id. Tell me the listing-id and I'll set up your Agent."*
93
+ This outputs a link to https://shll.run/setup with pre-filled parameters. Tell the user:
94
+ 1. *"Open the setup link in your browser."*
95
+ 2. *"Connect YOUR personal wallet (MetaMask, WalletConnect) — this becomes the owner wallet."*
96
+ 3. *"Follow the steps to: rent an agent → authorize the operator wallet → fund the vault."*
97
+ 4. *"When done, tell me your token-id number."*
71
98
 
72
- After init succeeds, tell the user:
73
- *"Your Agent #<tokenId> is ready! Your trading vault is at `<vault>`. I've funded it with 0.1 BNB. You can start trading now."*
99
+ **⚠️ NEVER use `init` for new users.** The `init` command is deprecated because it uses the same key for owner and operator, which is a security risk.
74
100
 
75
- **If YES** -> Verify it works:
101
+ **If YES** Verify:
76
102
  ```bash
77
103
  shll-run portfolio -k <ID>
78
104
  ```
@@ -89,16 +115,13 @@ The user is now set up. They can ask you things like:
89
115
 
90
116
  ## COMMAND REFERENCE
91
117
 
92
- ### Wallet Setup (no private key needed)
118
+ ### Wallet & Setup
93
119
  | Command | What it does |
94
120
  |---------|-------------|
95
- | `shll-run generate-wallet` | Create a new gas wallet (address + private key) |
96
- | `shll-run balance` | Check gas wallet BNB balance |
97
-
98
- ### Setup (one-time)
99
- | Command | What it does |
100
- |---------|-------------|
101
- | `shll-run init --listing-id <ID> --days <N> [--fund <BNB>]` | Rent agent + authorize + fund vault |
121
+ | `shll-run generate-wallet` | Create a new operator wallet (address + private key) |
122
+ | `shll-run balance` | Check operator wallet BNB balance |
123
+ | `shll-run setup-guide -l <LISTING> -d <DAYS>` | Output setup instructions + shll.run link for secure onboarding |
124
+ | ~~`shll-run init`~~ | **DEPRECATED** — insecure single-wallet mode |
102
125
 
103
126
  ### Trading
104
127
  | Command | What it does |
@@ -130,10 +153,13 @@ The user is now set up. They can ask you things like:
130
153
  ## HOW TO EXPLAIN THINGS TO USERS
131
154
 
132
155
  ### "What is RUNNER_PRIVATE_KEY?"
133
- *"It's a gas-paying wallet. Think of it like a prepaid card that only pays small transaction fees (~$0.01 each). It does NOT hold your trading money. Your trading funds are in a secure on-chain vault that this key can't directly access. You only need about $1 of BNB in it."*
156
+ *"It's your operator wallet a hot wallet that the AI uses to execute trades within safety limits. Think of it like a company credit card with a spending cap. It can NOT withdraw funds from your vault or transfer your agent ownership. You only need ~$1 of BNB in it for gas fees."*
157
+
158
+ ### "Why do I need two wallets?"
159
+ *"Security. Your personal wallet (owner) controls high-risk operations like withdrawing vault funds. The operator wallet can only trade within PolicyGuard limits. Even if the operator key is compromised, an attacker can NOT drain your vault — they can only make trades within the safety rules you've set. Your owner wallet stays offline and safe."*
134
160
 
135
161
  ### "Is my money safe?"
136
- *"Yes. All trading funds are in an on-chain vault protected by PolicyGuard, a smart contract that enforces spending limits, cooldowns, and whitelisted DEXs. Even if the AI makes a mistake, the smart contract will reject unsafe transactions. The gas wallet key cannot withdraw from the vault."*
162
+ *"Yes, on multiple levels. First, the operator wallet (which the AI uses) cannot withdraw vault funds only your owner wallet can. Second, all trades go through PolicyGuard, which enforces spending limits, cooldowns, and DEX whitelists. Even if someone got the operator key, your money is protected by on-chain smart contract rules."*
137
163
 
138
164
  ### "What are policies?"
139
165
  *"Policies are on-chain safety rules: how much you can spend per transaction, how often you can trade, which DEXs are allowed, etc. You can tighten these rules but never loosen them beyond the template ceiling."*
package/dist/index.js CHANGED
@@ -858,9 +858,16 @@ tokensCmd.action(() => {
858
858
  }));
859
859
  output({ status: "success", tokens });
860
860
  });
861
- var initCmd = new import_commander.Command("init").description("One-click setup: rent an Agent, authorize self as operator, and fund the vault").requiredOption("-l, --listing-id <bytes32>", "Template listing ID (bytes32 hex)").requiredOption("-d, --days <number>", "Number of days to rent").option("-f, --fund <bnb>", "BNB to deposit into vault (human-readable, e.g. 0.1)", "0").option("-r, --rpc <url>", "BSC RPC URL", DEFAULT_RPC).option("--listing-manager <address>", "ListingManagerV2 address", DEFAULT_LISTING_MANAGER).option("--nfa-address <address>", "AgentNFA contract address", DEFAULT_NFA);
861
+ var initCmd = new import_commander.Command("init").description("[DEPRECATED] One-click setup \u2014 uses same key as owner AND operator. Use setup-guide instead.").requiredOption("-l, --listing-id <bytes32>", "Template listing ID (bytes32 hex)").requiredOption("-d, --days <number>", "Number of days to rent").option("-f, --fund <bnb>", "BNB to deposit into vault (human-readable, e.g. 0.1)", "0").option("--i-understand-the-risk", "Acknowledge the security risk of using same key for owner and operator").option("-r, --rpc <url>", "BSC RPC URL", DEFAULT_RPC).option("--listing-manager <address>", "ListingManagerV2 address", DEFAULT_LISTING_MANAGER).option("--nfa-address <address>", "AgentNFA contract address", DEFAULT_NFA);
862
862
  initCmd.action(async (opts) => {
863
863
  try {
864
+ if (!opts.iUnderstandTheRisk) {
865
+ output({
866
+ status: "error",
867
+ message: "\u26A0\uFE0F DEPRECATED: 'init' uses the same private key as both owner AND operator. If this key is leaked (e.g. via AI prompt injection), an attacker can withdraw ALL vault funds. Use 'setup-guide' instead for a secure dual-wallet setup. If you understand the risk and still want to proceed, add --i-understand-the-risk."
868
+ });
869
+ process.exit(1);
870
+ }
864
871
  if (!process.env.RUNNER_PRIVATE_KEY) {
865
872
  output({ status: "error", message: "RUNNER_PRIVATE_KEY environment variable is missing" });
866
873
  process.exit(1);
@@ -1459,14 +1466,125 @@ configCmd.action(async (opts) => {
1459
1466
  process.exit(1);
1460
1467
  }
1461
1468
  });
1462
- var genWalletCmd = new import_commander.Command("generate-wallet").description("Generate a new BSC wallet (address + private key)").action(() => {
1469
+ var setupGuideCmd = new import_commander.Command("setup-guide").description("Output step-by-step instructions for secure dual-wallet agent setup").requiredOption("-l, --listing-id <bytes32>", "Template listing ID (bytes32 hex)").requiredOption("-d, --days <number>", "Number of days to rent").option("-r, --rpc <url>", "BSC RPC URL", DEFAULT_RPC).option("--listing-manager <address>", "ListingManagerV2 address", DEFAULT_LISTING_MANAGER).option("--nfa-address <address>", "AgentNFA contract address", DEFAULT_NFA);
1470
+ setupGuideCmd.action(async (opts) => {
1471
+ try {
1472
+ const pk = process.env.RUNNER_PRIVATE_KEY;
1473
+ let operatorAddress;
1474
+ if (pk) {
1475
+ const account = (0, import_accounts2.privateKeyToAccount)(toHex(pk));
1476
+ operatorAddress = account.address;
1477
+ } else {
1478
+ output({
1479
+ status: "error",
1480
+ message: "RUNNER_PRIVATE_KEY not set. Run 'generate-wallet' first to create an operator wallet, then set RUNNER_PRIVATE_KEY."
1481
+ });
1482
+ process.exit(1);
1483
+ return;
1484
+ }
1485
+ const rpcUrl = opts.rpc || DEFAULT_RPC;
1486
+ const listingManagerAddr = toHex(opts.listingManager || DEFAULT_LISTING_MANAGER);
1487
+ const nfaAddr = toHex(opts.nfaAddress || DEFAULT_NFA);
1488
+ const listingId = opts.listingId;
1489
+ const daysToRent = Number(opts.days);
1490
+ const publicClient = (0, import_viem2.createPublicClient)({ chain: import_chains2.bsc, transport: (0, import_viem2.http)(rpcUrl) });
1491
+ let rentCost = "unknown";
1492
+ let priceInfo = "";
1493
+ try {
1494
+ const listing = await publicClient.readContract({
1495
+ address: listingManagerAddr,
1496
+ abi: LISTING_MANAGER_ABI,
1497
+ functionName: "listings",
1498
+ args: [listingId]
1499
+ });
1500
+ const [, , , pricePerDay, minDays, active] = listing;
1501
+ if (!active) {
1502
+ output({ status: "error", message: "Listing is not active" });
1503
+ process.exit(1);
1504
+ }
1505
+ if (daysToRent < minDays) {
1506
+ output({ status: "error", message: `Minimum rental is ${minDays} days, you requested ${daysToRent}` });
1507
+ process.exit(1);
1508
+ }
1509
+ const totalRent = BigInt(pricePerDay) * BigInt(daysToRent);
1510
+ rentCost = `${(Number(totalRent) / 1e18).toFixed(6)} BNB`;
1511
+ priceInfo = ` (${totalRent.toString()} wei)`;
1512
+ } catch {
1513
+ rentCost = "unable to query \u2014 check listing-id";
1514
+ }
1515
+ const expiryTimestamp = Math.floor(Date.now() / 1e3) + daysToRent * 86400;
1516
+ const setupUrl = `https://shll.run/setup?operator=${operatorAddress}&listing=${encodeURIComponent(listingId)}&days=${daysToRent}`;
1517
+ output({
1518
+ status: "guide",
1519
+ securityModel: "DUAL-WALLET: Your wallet (owner) stays offline. AI only uses the operator wallet, which CANNOT withdraw vault funds.",
1520
+ operatorAddress,
1521
+ setupUrl,
1522
+ rentCost: `${rentCost}${priceInfo}`,
1523
+ steps: [
1524
+ {
1525
+ step: 1,
1526
+ title: "Open SHLL Setup Page",
1527
+ action: `Open ${setupUrl} in your browser`,
1528
+ note: "Connect YOUR wallet (MetaMask/WalletConnect). This is your owner wallet \u2014 keep it safe and offline after setup."
1529
+ },
1530
+ {
1531
+ step: 2,
1532
+ title: "Rent Agent",
1533
+ action: "Click 'Rent Agent' and confirm the transaction",
1534
+ fallback: {
1535
+ method: "BscScan (manual)",
1536
+ contract: listingManagerAddr,
1537
+ function: "rentToMintWithParams(bytes32,uint32,uint32,uint16,bytes)",
1538
+ args: [listingId, daysToRent, 1, 1, "0x01"],
1539
+ value: rentCost
1540
+ }
1541
+ },
1542
+ {
1543
+ step: 3,
1544
+ title: "Authorize Operator",
1545
+ action: "Click 'Authorize Operator' \u2014 this gives the AI wallet permission to trade within PolicyGuard safety limits",
1546
+ note: `Operator address: ${operatorAddress}`,
1547
+ fallback: {
1548
+ method: "BscScan (manual)",
1549
+ contract: nfaAddr,
1550
+ function: "setOperator(uint256,address,uint64)",
1551
+ args: ["<tokenId from step 2>", operatorAddress, expiryTimestamp]
1552
+ }
1553
+ },
1554
+ {
1555
+ step: 4,
1556
+ title: "Fund Vault (optional)",
1557
+ action: "Deposit BNB into the vault for trading",
1558
+ fallback: {
1559
+ method: "BscScan (manual)",
1560
+ contract: nfaAddr,
1561
+ function: "fundAgent(uint256)",
1562
+ args: ["<tokenId>"],
1563
+ value: "amount of BNB to deposit"
1564
+ }
1565
+ },
1566
+ {
1567
+ step: 5,
1568
+ title: "Tell AI your token-id",
1569
+ action: "Come back and tell the AI your token-id number. The AI will verify your portfolio and you're ready to trade."
1570
+ }
1571
+ ]
1572
+ });
1573
+ } catch (error) {
1574
+ const message = error instanceof Error ? error.message : "Unknown error";
1575
+ output({ status: "error", message });
1576
+ process.exit(1);
1577
+ }
1578
+ });
1579
+ var genWalletCmd = new import_commander.Command("generate-wallet").description("Generate a new operator wallet (address + private key) for AI to use").action(() => {
1463
1580
  const pk = (0, import_accounts2.generatePrivateKey)();
1464
1581
  const account = (0, import_accounts2.privateKeyToAccount)(pk);
1465
1582
  output({
1466
1583
  status: "success",
1467
1584
  address: account.address,
1468
1585
  privateKey: pk,
1469
- note: "SAVE THIS PRIVATE KEY SECURELY. This wallet is for paying gas fees (~$0.01/tx). Send ~$1 of BNB to the address above, then set RUNNER_PRIVATE_KEY to the privateKey value."
1586
+ note: "SAVE THIS PRIVATE KEY SECURELY. This is the OPERATOR wallet \u2014 it can only trade within PolicyGuard limits. It CANNOT withdraw vault funds or transfer your Agent NFT. Send ~$1 of BNB here for gas fees, then set RUNNER_PRIVATE_KEY to this privateKey value.",
1587
+ securityReminder: "Use a SEPARATE wallet (MetaMask, hardware wallet) as the owner to rent the agent and fund the vault. Run 'setup-guide' for step-by-step instructions."
1470
1588
  });
1471
1589
  });
1472
1590
  var balanceCmd = new import_commander.Command("balance").description("Check BNB balance of the gas-paying wallet (RUNNER_PRIVATE_KEY)").option("-r, --rpc <url>", "BSC RPC URL", DEFAULT_RPC).action(async (opts) => {
@@ -1507,6 +1625,7 @@ program.addCommand(unwrapCmd);
1507
1625
  program.addCommand(transferCmd);
1508
1626
  program.addCommand(policiesCmd);
1509
1627
  program.addCommand(configCmd);
1628
+ program.addCommand(setupGuideCmd);
1510
1629
  program.addCommand(genWalletCmd);
1511
1630
  program.addCommand(balanceCmd);
1512
1631
  program.parse(process.argv);
package/dist/index.mjs CHANGED
@@ -868,9 +868,16 @@ tokensCmd.action(() => {
868
868
  }));
869
869
  output({ status: "success", tokens });
870
870
  });
871
- var initCmd = new Command("init").description("One-click setup: rent an Agent, authorize self as operator, and fund the vault").requiredOption("-l, --listing-id <bytes32>", "Template listing ID (bytes32 hex)").requiredOption("-d, --days <number>", "Number of days to rent").option("-f, --fund <bnb>", "BNB to deposit into vault (human-readable, e.g. 0.1)", "0").option("-r, --rpc <url>", "BSC RPC URL", DEFAULT_RPC).option("--listing-manager <address>", "ListingManagerV2 address", DEFAULT_LISTING_MANAGER).option("--nfa-address <address>", "AgentNFA contract address", DEFAULT_NFA);
871
+ var initCmd = new Command("init").description("[DEPRECATED] One-click setup \u2014 uses same key as owner AND operator. Use setup-guide instead.").requiredOption("-l, --listing-id <bytes32>", "Template listing ID (bytes32 hex)").requiredOption("-d, --days <number>", "Number of days to rent").option("-f, --fund <bnb>", "BNB to deposit into vault (human-readable, e.g. 0.1)", "0").option("--i-understand-the-risk", "Acknowledge the security risk of using same key for owner and operator").option("-r, --rpc <url>", "BSC RPC URL", DEFAULT_RPC).option("--listing-manager <address>", "ListingManagerV2 address", DEFAULT_LISTING_MANAGER).option("--nfa-address <address>", "AgentNFA contract address", DEFAULT_NFA);
872
872
  initCmd.action(async (opts) => {
873
873
  try {
874
+ if (!opts.iUnderstandTheRisk) {
875
+ output({
876
+ status: "error",
877
+ message: "\u26A0\uFE0F DEPRECATED: 'init' uses the same private key as both owner AND operator. If this key is leaked (e.g. via AI prompt injection), an attacker can withdraw ALL vault funds. Use 'setup-guide' instead for a secure dual-wallet setup. If you understand the risk and still want to proceed, add --i-understand-the-risk."
878
+ });
879
+ process.exit(1);
880
+ }
874
881
  if (!process.env.RUNNER_PRIVATE_KEY) {
875
882
  output({ status: "error", message: "RUNNER_PRIVATE_KEY environment variable is missing" });
876
883
  process.exit(1);
@@ -1469,14 +1476,125 @@ configCmd.action(async (opts) => {
1469
1476
  process.exit(1);
1470
1477
  }
1471
1478
  });
1472
- var genWalletCmd = new Command("generate-wallet").description("Generate a new BSC wallet (address + private key)").action(() => {
1479
+ var setupGuideCmd = new Command("setup-guide").description("Output step-by-step instructions for secure dual-wallet agent setup").requiredOption("-l, --listing-id <bytes32>", "Template listing ID (bytes32 hex)").requiredOption("-d, --days <number>", "Number of days to rent").option("-r, --rpc <url>", "BSC RPC URL", DEFAULT_RPC).option("--listing-manager <address>", "ListingManagerV2 address", DEFAULT_LISTING_MANAGER).option("--nfa-address <address>", "AgentNFA contract address", DEFAULT_NFA);
1480
+ setupGuideCmd.action(async (opts) => {
1481
+ try {
1482
+ const pk = process.env.RUNNER_PRIVATE_KEY;
1483
+ let operatorAddress;
1484
+ if (pk) {
1485
+ const account = privateKeyToAccount2(toHex(pk));
1486
+ operatorAddress = account.address;
1487
+ } else {
1488
+ output({
1489
+ status: "error",
1490
+ message: "RUNNER_PRIVATE_KEY not set. Run 'generate-wallet' first to create an operator wallet, then set RUNNER_PRIVATE_KEY."
1491
+ });
1492
+ process.exit(1);
1493
+ return;
1494
+ }
1495
+ const rpcUrl = opts.rpc || DEFAULT_RPC;
1496
+ const listingManagerAddr = toHex(opts.listingManager || DEFAULT_LISTING_MANAGER);
1497
+ const nfaAddr = toHex(opts.nfaAddress || DEFAULT_NFA);
1498
+ const listingId = opts.listingId;
1499
+ const daysToRent = Number(opts.days);
1500
+ const publicClient = createPublicClient2({ chain: bsc2, transport: http2(rpcUrl) });
1501
+ let rentCost = "unknown";
1502
+ let priceInfo = "";
1503
+ try {
1504
+ const listing = await publicClient.readContract({
1505
+ address: listingManagerAddr,
1506
+ abi: LISTING_MANAGER_ABI,
1507
+ functionName: "listings",
1508
+ args: [listingId]
1509
+ });
1510
+ const [, , , pricePerDay, minDays, active] = listing;
1511
+ if (!active) {
1512
+ output({ status: "error", message: "Listing is not active" });
1513
+ process.exit(1);
1514
+ }
1515
+ if (daysToRent < minDays) {
1516
+ output({ status: "error", message: `Minimum rental is ${minDays} days, you requested ${daysToRent}` });
1517
+ process.exit(1);
1518
+ }
1519
+ const totalRent = BigInt(pricePerDay) * BigInt(daysToRent);
1520
+ rentCost = `${(Number(totalRent) / 1e18).toFixed(6)} BNB`;
1521
+ priceInfo = ` (${totalRent.toString()} wei)`;
1522
+ } catch {
1523
+ rentCost = "unable to query \u2014 check listing-id";
1524
+ }
1525
+ const expiryTimestamp = Math.floor(Date.now() / 1e3) + daysToRent * 86400;
1526
+ const setupUrl = `https://shll.run/setup?operator=${operatorAddress}&listing=${encodeURIComponent(listingId)}&days=${daysToRent}`;
1527
+ output({
1528
+ status: "guide",
1529
+ securityModel: "DUAL-WALLET: Your wallet (owner) stays offline. AI only uses the operator wallet, which CANNOT withdraw vault funds.",
1530
+ operatorAddress,
1531
+ setupUrl,
1532
+ rentCost: `${rentCost}${priceInfo}`,
1533
+ steps: [
1534
+ {
1535
+ step: 1,
1536
+ title: "Open SHLL Setup Page",
1537
+ action: `Open ${setupUrl} in your browser`,
1538
+ note: "Connect YOUR wallet (MetaMask/WalletConnect). This is your owner wallet \u2014 keep it safe and offline after setup."
1539
+ },
1540
+ {
1541
+ step: 2,
1542
+ title: "Rent Agent",
1543
+ action: "Click 'Rent Agent' and confirm the transaction",
1544
+ fallback: {
1545
+ method: "BscScan (manual)",
1546
+ contract: listingManagerAddr,
1547
+ function: "rentToMintWithParams(bytes32,uint32,uint32,uint16,bytes)",
1548
+ args: [listingId, daysToRent, 1, 1, "0x01"],
1549
+ value: rentCost
1550
+ }
1551
+ },
1552
+ {
1553
+ step: 3,
1554
+ title: "Authorize Operator",
1555
+ action: "Click 'Authorize Operator' \u2014 this gives the AI wallet permission to trade within PolicyGuard safety limits",
1556
+ note: `Operator address: ${operatorAddress}`,
1557
+ fallback: {
1558
+ method: "BscScan (manual)",
1559
+ contract: nfaAddr,
1560
+ function: "setOperator(uint256,address,uint64)",
1561
+ args: ["<tokenId from step 2>", operatorAddress, expiryTimestamp]
1562
+ }
1563
+ },
1564
+ {
1565
+ step: 4,
1566
+ title: "Fund Vault (optional)",
1567
+ action: "Deposit BNB into the vault for trading",
1568
+ fallback: {
1569
+ method: "BscScan (manual)",
1570
+ contract: nfaAddr,
1571
+ function: "fundAgent(uint256)",
1572
+ args: ["<tokenId>"],
1573
+ value: "amount of BNB to deposit"
1574
+ }
1575
+ },
1576
+ {
1577
+ step: 5,
1578
+ title: "Tell AI your token-id",
1579
+ action: "Come back and tell the AI your token-id number. The AI will verify your portfolio and you're ready to trade."
1580
+ }
1581
+ ]
1582
+ });
1583
+ } catch (error) {
1584
+ const message = error instanceof Error ? error.message : "Unknown error";
1585
+ output({ status: "error", message });
1586
+ process.exit(1);
1587
+ }
1588
+ });
1589
+ var genWalletCmd = new Command("generate-wallet").description("Generate a new operator wallet (address + private key) for AI to use").action(() => {
1473
1590
  const pk = generatePrivateKey();
1474
1591
  const account = privateKeyToAccount2(pk);
1475
1592
  output({
1476
1593
  status: "success",
1477
1594
  address: account.address,
1478
1595
  privateKey: pk,
1479
- note: "SAVE THIS PRIVATE KEY SECURELY. This wallet is for paying gas fees (~$0.01/tx). Send ~$1 of BNB to the address above, then set RUNNER_PRIVATE_KEY to the privateKey value."
1596
+ note: "SAVE THIS PRIVATE KEY SECURELY. This is the OPERATOR wallet \u2014 it can only trade within PolicyGuard limits. It CANNOT withdraw vault funds or transfer your Agent NFT. Send ~$1 of BNB here for gas fees, then set RUNNER_PRIVATE_KEY to this privateKey value.",
1597
+ securityReminder: "Use a SEPARATE wallet (MetaMask, hardware wallet) as the owner to rent the agent and fund the vault. Run 'setup-guide' for step-by-step instructions."
1480
1598
  });
1481
1599
  });
1482
1600
  var balanceCmd = new Command("balance").description("Check BNB balance of the gas-paying wallet (RUNNER_PRIVATE_KEY)").option("-r, --rpc <url>", "BSC RPC URL", DEFAULT_RPC).action(async (opts) => {
@@ -1517,6 +1635,7 @@ program.addCommand(unwrapCmd);
1517
1635
  program.addCommand(transferCmd);
1518
1636
  program.addCommand(policiesCmd);
1519
1637
  program.addCommand(configCmd);
1638
+ program.addCommand(setupGuideCmd);
1520
1639
  program.addCommand(genWalletCmd);
1521
1640
  program.addCommand(balanceCmd);
1522
1641
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shll-skills",
3
- "version": "1.3.0",
3
+ "version": "2.0.0",
4
4
  "description": "SHLL Agent Runtime Skill for OpenClaw",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -21,4 +21,4 @@
21
21
  "typescript": "^5.4.5",
22
22
  "@types/node": "^20.12.7"
23
23
  }
24
- }
24
+ }
package/src/index.ts CHANGED
@@ -438,18 +438,31 @@ tokensCmd.action(() => {
438
438
  output({ status: "success", tokens });
439
439
  });
440
440
 
441
- // ── Subcommand: init (one-click onboarding) ─────────────
441
+ // ── Subcommand: init (DEPRECATED — uses same key for owner+operator) ──
442
442
  const initCmd = new Command("init")
443
- .description("One-click setup: rent an Agent, authorize self as operator, and fund the vault")
443
+ .description("[DEPRECATED] One-click setup uses same key as owner AND operator. Use setup-guide instead.")
444
444
  .requiredOption("-l, --listing-id <bytes32>", "Template listing ID (bytes32 hex)")
445
445
  .requiredOption("-d, --days <number>", "Number of days to rent")
446
446
  .option("-f, --fund <bnb>", "BNB to deposit into vault (human-readable, e.g. 0.1)", "0")
447
+ .option("--i-understand-the-risk", "Acknowledge the security risk of using same key for owner and operator")
447
448
  .option("-r, --rpc <url>", "BSC RPC URL", DEFAULT_RPC)
448
449
  .option("--listing-manager <address>", "ListingManagerV2 address", DEFAULT_LISTING_MANAGER)
449
450
  .option("--nfa-address <address>", "AgentNFA contract address", DEFAULT_NFA);
450
451
 
451
452
  initCmd.action(async (opts) => {
452
453
  try {
454
+ // Deprecation guard
455
+ if (!opts.iUnderstandTheRisk) {
456
+ output({
457
+ status: "error",
458
+ message: "⚠️ DEPRECATED: 'init' uses the same private key as both owner AND operator. " +
459
+ "If this key is leaked (e.g. via AI prompt injection), an attacker can withdraw ALL vault funds. " +
460
+ "Use 'setup-guide' instead for a secure dual-wallet setup. " +
461
+ "If you understand the risk and still want to proceed, add --i-understand-the-risk.",
462
+ });
463
+ process.exit(1);
464
+ }
465
+
453
466
  // Validate private key
454
467
  if (!process.env.RUNNER_PRIVATE_KEY) {
455
468
  output({ status: "error", message: "RUNNER_PRIVATE_KEY environment variable is missing" });
@@ -1227,9 +1240,138 @@ configCmd.action(async (opts) => {
1227
1240
  }
1228
1241
  });
1229
1242
 
1243
+ // -- Subcommand: setup-guide (secure dual-wallet onboarding) ------
1244
+ const setupGuideCmd = new Command("setup-guide")
1245
+ .description("Output step-by-step instructions for secure dual-wallet agent setup")
1246
+ .requiredOption("-l, --listing-id <bytes32>", "Template listing ID (bytes32 hex)")
1247
+ .requiredOption("-d, --days <number>", "Number of days to rent")
1248
+ .option("-r, --rpc <url>", "BSC RPC URL", DEFAULT_RPC)
1249
+ .option("--listing-manager <address>", "ListingManagerV2 address", DEFAULT_LISTING_MANAGER)
1250
+ .option("--nfa-address <address>", "AgentNFA contract address", DEFAULT_NFA);
1251
+
1252
+ setupGuideCmd.action(async (opts) => {
1253
+ try {
1254
+ // Get operator address from RUNNER_PRIVATE_KEY
1255
+ const pk = process.env.RUNNER_PRIVATE_KEY;
1256
+ let operatorAddress: string;
1257
+ if (pk) {
1258
+ const account = privateKeyToAccount(toHex(pk) as Hex);
1259
+ operatorAddress = account.address;
1260
+ } else {
1261
+ output({
1262
+ status: "error",
1263
+ message: "RUNNER_PRIVATE_KEY not set. Run 'generate-wallet' first to create an operator wallet, then set RUNNER_PRIVATE_KEY.",
1264
+ });
1265
+ process.exit(1);
1266
+ return;
1267
+ }
1268
+
1269
+ const rpcUrl = opts.rpc || DEFAULT_RPC;
1270
+ const listingManagerAddr = toHex(opts.listingManager || DEFAULT_LISTING_MANAGER) as Address;
1271
+ const nfaAddr = toHex(opts.nfaAddress || DEFAULT_NFA) as Address;
1272
+ const listingId = opts.listingId as string;
1273
+ const daysToRent = Number(opts.days);
1274
+
1275
+ // Query listing to calculate rent cost
1276
+ const publicClient = createPublicClient({ chain: bsc, transport: http(rpcUrl) });
1277
+ let rentCost = "unknown";
1278
+ let priceInfo = "";
1279
+ try {
1280
+ const listing = await publicClient.readContract({
1281
+ address: listingManagerAddr,
1282
+ abi: LISTING_MANAGER_ABI,
1283
+ functionName: "listings",
1284
+ args: [listingId as Hex],
1285
+ });
1286
+ const [, , , pricePerDay, minDays, active] = listing;
1287
+ if (!active) {
1288
+ output({ status: "error", message: "Listing is not active" });
1289
+ process.exit(1);
1290
+ }
1291
+ if (daysToRent < minDays) {
1292
+ output({ status: "error", message: `Minimum rental is ${minDays} days, you requested ${daysToRent}` });
1293
+ process.exit(1);
1294
+ }
1295
+ const totalRent = BigInt(pricePerDay) * BigInt(daysToRent);
1296
+ rentCost = `${(Number(totalRent) / 1e18).toFixed(6)} BNB`;
1297
+ priceInfo = ` (${totalRent.toString()} wei)`;
1298
+ } catch {
1299
+ rentCost = "unable to query — check listing-id";
1300
+ }
1301
+
1302
+ // Calculate operator expiry timestamp
1303
+ const expiryTimestamp = Math.floor(Date.now() / 1000) + daysToRent * 86400;
1304
+
1305
+ // Build shll.run setup URL
1306
+ const setupUrl = `https://shll.run/setup?operator=${operatorAddress}&listing=${encodeURIComponent(listingId)}&days=${daysToRent}`;
1307
+
1308
+ output({
1309
+ status: "guide",
1310
+ securityModel: "DUAL-WALLET: Your wallet (owner) stays offline. AI only uses the operator wallet, which CANNOT withdraw vault funds.",
1311
+ operatorAddress,
1312
+ setupUrl,
1313
+ rentCost: `${rentCost}${priceInfo}`,
1314
+ steps: [
1315
+ {
1316
+ step: 1,
1317
+ title: "Open SHLL Setup Page",
1318
+ action: `Open ${setupUrl} in your browser`,
1319
+ note: "Connect YOUR wallet (MetaMask/WalletConnect). This is your owner wallet — keep it safe and offline after setup.",
1320
+ },
1321
+ {
1322
+ step: 2,
1323
+ title: "Rent Agent",
1324
+ action: "Click 'Rent Agent' and confirm the transaction",
1325
+ fallback: {
1326
+ method: "BscScan (manual)",
1327
+ contract: listingManagerAddr,
1328
+ function: "rentToMintWithParams(bytes32,uint32,uint32,uint16,bytes)",
1329
+ args: [listingId, daysToRent, 1, 1, "0x01"],
1330
+ value: rentCost,
1331
+ },
1332
+ },
1333
+ {
1334
+ step: 3,
1335
+ title: "Authorize Operator",
1336
+ action: "Click 'Authorize Operator' — this gives the AI wallet permission to trade within PolicyGuard safety limits",
1337
+ note: `Operator address: ${operatorAddress}`,
1338
+ fallback: {
1339
+ method: "BscScan (manual)",
1340
+ contract: nfaAddr,
1341
+ function: "setOperator(uint256,address,uint64)",
1342
+ args: ["<tokenId from step 2>", operatorAddress, expiryTimestamp],
1343
+ },
1344
+ },
1345
+ {
1346
+ step: 4,
1347
+ title: "Fund Vault (optional)",
1348
+ action: "Deposit BNB into the vault for trading",
1349
+ fallback: {
1350
+ method: "BscScan (manual)",
1351
+ contract: nfaAddr,
1352
+ function: "fundAgent(uint256)",
1353
+ args: ["<tokenId>"],
1354
+ value: "amount of BNB to deposit",
1355
+ },
1356
+ },
1357
+ {
1358
+ step: 5,
1359
+ title: "Tell AI your token-id",
1360
+ action: "Come back and tell the AI your token-id number. The AI will verify your portfolio and you're ready to trade.",
1361
+ },
1362
+ ],
1363
+ });
1364
+
1365
+ } catch (error: unknown) {
1366
+ const message = error instanceof Error ? error.message : "Unknown error";
1367
+ output({ status: "error", message });
1368
+ process.exit(1);
1369
+ }
1370
+ });
1371
+
1230
1372
  // -- Subcommand: generate-wallet ---------------------------------
1231
1373
  const genWalletCmd = new Command("generate-wallet")
1232
- .description("Generate a new BSC wallet (address + private key)")
1374
+ .description("Generate a new operator wallet (address + private key) for AI to use")
1233
1375
  .action(() => {
1234
1376
  const pk = generatePrivateKey();
1235
1377
  const account = privateKeyToAccount(pk);
@@ -1237,7 +1379,11 @@ const genWalletCmd = new Command("generate-wallet")
1237
1379
  status: "success",
1238
1380
  address: account.address,
1239
1381
  privateKey: pk,
1240
- note: "SAVE THIS PRIVATE KEY SECURELY. This wallet is for paying gas fees (~$0.01/tx). Send ~$1 of BNB to the address above, then set RUNNER_PRIVATE_KEY to the privateKey value.",
1382
+ note: "SAVE THIS PRIVATE KEY SECURELY. This is the OPERATOR wallet it can only trade within PolicyGuard limits. " +
1383
+ "It CANNOT withdraw vault funds or transfer your Agent NFT. " +
1384
+ "Send ~$1 of BNB here for gas fees, then set RUNNER_PRIVATE_KEY to this privateKey value.",
1385
+ securityReminder: "Use a SEPARATE wallet (MetaMask, hardware wallet) as the owner to rent the agent and fund the vault. " +
1386
+ "Run 'setup-guide' for step-by-step instructions.",
1241
1387
  });
1242
1388
  });
1243
1389
 
@@ -1286,6 +1432,7 @@ program.addCommand(unwrapCmd);
1286
1432
  program.addCommand(transferCmd);
1287
1433
  program.addCommand(policiesCmd);
1288
1434
  program.addCommand(configCmd);
1435
+ program.addCommand(setupGuideCmd);
1289
1436
  program.addCommand(genWalletCmd);
1290
1437
  program.addCommand(balanceCmd);
1291
1438
  program.parse(process.argv);