@xpr-agents/openclaw 0.4.2 → 0.4.3

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/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@xpr-agents/openclaw",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "OpenClaw plugin for XPR Network Trustless Agent Registry - autonomous agent operation, escrow jobs, feedback, and validation",
5
5
  "author": "XPR Network",
6
+ "bin": {
7
+ "xpr-agents-setup-security": "scripts/setup-security.sh"
8
+ },
6
9
  "repository": {
7
10
  "type": "git",
8
11
  "url": "git+https://github.com/XPRNetwork/xpr-agents.git",
@@ -0,0 +1,504 @@
1
+ #!/usr/bin/env bash
2
+ # setup-security.sh — Pillar 2 lockdown for an XPR Agents account.
3
+ #
4
+ # Delegates the agent's `owner` permission to a separate human-controlled
5
+ # account, so even if the agent's active key is compromised the attacker
6
+ # cannot rotate the account away from you.
7
+ #
8
+ # Idempotent: if owner is already controlled by a non-raw-key account
9
+ # permission, exits cleanly with no changes.
10
+ #
11
+ # Refuses to run unattended — TTY required, explicit yes/no on every
12
+ # prompt, type-to-confirm account names, hard-fails on any precondition
13
+ # (account doesn't exist, key not in keychain, etc).
14
+ #
15
+ # See docs/SECURITY.md for the full security model.
16
+
17
+ set -eu
18
+
19
+ # ── Colors ─────────────────────────────────────
20
+ if [ -t 1 ]; then
21
+ RED=$'\033[31m'
22
+ GREEN=$'\033[32m'
23
+ YELLOW=$'\033[33m'
24
+ BOLD=$'\033[1m'
25
+ NC=$'\033[0m'
26
+ else
27
+ RED=''; GREEN=''; YELLOW=''; BOLD=''; NC=''
28
+ fi
29
+
30
+ err() { printf "${RED}${BOLD}ERROR:${NC} %s\n" "$*" >&2; }
31
+ warn() { printf "${YELLOW}!${NC} %s\n" "$*"; }
32
+ ok() { printf "${GREEN}✓${NC} %s\n" "$*"; }
33
+ info() { printf " %s\n" "$*"; }
34
+ step() { printf "\n${BOLD}[%s]${NC} %s\n" "$1" "$2"; }
35
+ abort() { err "$*"; exit 1; }
36
+
37
+ # ── Hard preconditions ─────────────────────────
38
+
39
+ # Require a TTY. We will NOT run this from a pipe, a heredoc, or under
40
+ # automation. The whole point is human-in-the-loop.
41
+ if [ ! -t 0 ] || [ ! -t 1 ]; then
42
+ err "setup-security.sh requires an interactive terminal."
43
+ err "Do not pipe input or run this from automation."
44
+ exit 1
45
+ fi
46
+
47
+ # Require proton CLI on PATH.
48
+ if ! command -v proton >/dev/null 2>&1; then
49
+ err "proton CLI not found on PATH."
50
+ info "Install it: npm i -g @proton/cli"
51
+ info "Then add the npm global bin to PATH:"
52
+ info " export PATH=\"\$(npm config get prefix)/bin:\$PATH\""
53
+ exit 1
54
+ fi
55
+
56
+ # Require XPR_ACCOUNT or --account arg.
57
+ AGENT_ACCOUNT="${XPR_ACCOUNT:-}"
58
+ while [ $# -gt 0 ]; do
59
+ case "$1" in
60
+ --account) AGENT_ACCOUNT="$2"; shift 2 ;;
61
+ --help|-h)
62
+ cat <<'EOF'
63
+ Usage: ./setup-security.sh [--account <agent-account>]
64
+
65
+ Locks down the agent's `owner` permission so that only a human-controlled
66
+ XPR account can change permissions. Run once per agent. Idempotent.
67
+
68
+ The agent's `active` key stays in the proton CLI keychain — daily signing
69
+ is unchanged. Only the `owner` permission moves to your human account.
70
+
71
+ See docs/SECURITY.md for the full rationale.
72
+ EOF
73
+ exit 0 ;;
74
+ *) shift ;;
75
+ esac
76
+ done
77
+
78
+ if [ -z "$AGENT_ACCOUNT" ]; then
79
+ err "Agent account not specified."
80
+ info "Pass --account <name> or set XPR_ACCOUNT in env."
81
+ exit 1
82
+ fi
83
+
84
+ # Validate name shape (EOSIO: 1-12 chars, .12345abcdefghijklmnopqrstuvwxyz)
85
+ if ! printf '%s' "$AGENT_ACCOUNT" | grep -qE '^[.1-5a-z]{1,12}$'; then
86
+ abort "'$AGENT_ACCOUNT' is not a valid XPR Network account name (1-12 chars from .12345a-z)."
87
+ fi
88
+
89
+ # ── Banner ─────────────────────────────────────
90
+ cat <<EOF
91
+
92
+ ========================================================================
93
+ ${BOLD}XPR AGENTS — SECURITY SETUP${NC} (Pillar 2: lock down owner)
94
+ ========================================================================
95
+
96
+ Target agent account: ${BOLD}${AGENT_ACCOUNT}${NC}
97
+
98
+ This script will delegate '$AGENT_ACCOUNT's owner permission to a
99
+ separate human-controlled XPR account. After this:
100
+
101
+ • Your human account controls recovery if the agent's key leaks.
102
+ • The agent's active key stays in the proton CLI keychain — daily
103
+ signing is unchanged.
104
+ • The agent's owner permission will have NO raw keys — only your
105
+ human account can change permissions.
106
+
107
+ This is recommended but not automatic. See docs/SECURITY.md for the
108
+ full security model.
109
+
110
+ EOF
111
+
112
+ # ── Step 1: Read current account state ─────────
113
+ step "1/6" "Reading current state for '$AGENT_ACCOUNT'..."
114
+
115
+ if ! ACCOUNT_JSON=$(proton account "$AGENT_ACCOUNT" --json 2>/dev/null); then
116
+ # Fallback: --json may not be supported on older proton CLI versions
117
+ if ! ACCOUNT_OUT=$(proton account "$AGENT_ACCOUNT" 2>&1); then
118
+ err "Failed to look up account '$AGENT_ACCOUNT'."
119
+ info "Does the account exist? Check: proton account $AGENT_ACCOUNT"
120
+ info "Are you on the right chain? Check: proton chain"
121
+ exit 1
122
+ fi
123
+ # No JSON support — fall back to text parsing. Less robust.
124
+ ACCOUNT_JSON=""
125
+ fi
126
+
127
+ # Extract permissions. Prefer JSON if available.
128
+ if [ -n "$ACCOUNT_JSON" ]; then
129
+ OWNER_KEYS=$(printf '%s' "$ACCOUNT_JSON" | node -e "
130
+ let s=''; process.stdin.on('data',d=>s+=d); process.stdin.on('end',()=>{
131
+ try {
132
+ const a = JSON.parse(s);
133
+ const owner = (a.permissions||[]).find(p=>p.perm_name==='owner');
134
+ if (!owner) { console.log(''); return; }
135
+ const keys = (owner.required_auth?.keys||[]).map(k=>k.key);
136
+ console.log(keys.join(','));
137
+ } catch(e) { console.log(''); }
138
+ });
139
+ " <<< "$ACCOUNT_JSON")
140
+ OWNER_ACCOUNTS=$(printf '%s' "$ACCOUNT_JSON" | node -e "
141
+ let s=''; process.stdin.on('data',d=>s+=d); process.stdin.on('end',()=>{
142
+ try {
143
+ const a = JSON.parse(s);
144
+ const owner = (a.permissions||[]).find(p=>p.perm_name==='owner');
145
+ if (!owner) { console.log(''); return; }
146
+ const accts = (owner.required_auth?.accounts||[]).map(x=>x.permission.actor+'@'+x.permission.permission);
147
+ console.log(accts.join(','));
148
+ } catch(e) { console.log(''); }
149
+ });
150
+ " <<< "$ACCOUNT_JSON")
151
+ ACTIVE_KEYS=$(printf '%s' "$ACCOUNT_JSON" | node -e "
152
+ let s=''; process.stdin.on('data',d=>s+=d); process.stdin.on('end',()=>{
153
+ try {
154
+ const a = JSON.parse(s);
155
+ const active = (a.permissions||[]).find(p=>p.perm_name==='active');
156
+ if (!active) { console.log(''); return; }
157
+ const keys = (active.required_auth?.keys||[]).map(k=>k.key);
158
+ console.log(keys.join(','));
159
+ } catch(e) { console.log(''); }
160
+ });
161
+ " <<< "$ACCOUNT_JSON")
162
+ else
163
+ abort "proton account output format not recognized. Update proton CLI: npm i -g @proton/cli"
164
+ fi
165
+
166
+ info "owner perm keys: ${OWNER_KEYS:-(none)}"
167
+ info "owner perm accounts: ${OWNER_ACCOUNTS:-(none)}"
168
+ info "active perm keys: ${ACTIVE_KEYS:-(none)}"
169
+
170
+ # Idempotency check: if owner has no raw keys, we're already done.
171
+ if [ -z "$OWNER_KEYS" ] && [ -n "$OWNER_ACCOUNTS" ]; then
172
+ ok "Already secured. owner is controlled by: $OWNER_ACCOUNTS"
173
+ info "No changes needed. If you want to change the owner-controlling account,"
174
+ info "run a manual updateauth — see docs/SECURITY.md."
175
+ exit 0
176
+ fi
177
+
178
+ if [ -z "$OWNER_KEYS" ]; then
179
+ abort "owner permission has no keys AND no accounts? This is unusual. Check: proton account $AGENT_ACCOUNT"
180
+ fi
181
+
182
+ # We have raw keys on owner. Proceed.
183
+ warn "owner permission currently has raw keys. This is the default after account creation."
184
+ warn "If that key leaks, an attacker can rotate you out of your own account."
185
+
186
+ # ── Step 2: Verify we can sign as <agent>@owner ─
187
+ step "2/6" "Verifying we can sign as ${AGENT_ACCOUNT}@owner..."
188
+
189
+ KEYLIST=$(proton key:list 2>/dev/null || true)
190
+ MATCHED_OWNER_KEY=""
191
+ IFS=',' read -ra OWNER_KEY_ARR <<< "$OWNER_KEYS"
192
+ for k in "${OWNER_KEY_ARR[@]}"; do
193
+ if printf '%s' "$KEYLIST" | grep -qF "\"publicKey\": \"$k\""; then
194
+ MATCHED_OWNER_KEY="$k"
195
+ break
196
+ fi
197
+ done
198
+
199
+ if [ -z "$MATCHED_OWNER_KEY" ]; then
200
+ err "None of the owner-permission keys are loaded in the proton CLI keychain."
201
+ info "Owner keys on chain: $OWNER_KEYS"
202
+ info ""
203
+ info "Load the owner private key (PVT_K1_...) into the keychain:"
204
+ info " proton key:add"
205
+ info ""
206
+ info "For WebAuth-created accounts: the K1 backup key is in your wallet"
207
+ info "(WebAuth → Settings → Backup → reveal key)."
208
+ exit 1
209
+ fi
210
+ ok "Found matching key for ${AGENT_ACCOUNT}@owner: $MATCHED_OWNER_KEY"
211
+
212
+ # Determine the K1 we'll put on active. If active is currently a WA key
213
+ # (WebAuth biometric, unusable for autonomous signing), we replace it with
214
+ # the K1 currently on owner. If active is already a K1, we leave it alone.
215
+ NEW_ACTIVE_KEY=""
216
+ ACTIVE_NEEDS_REWRITE="no"
217
+
218
+ ACTIVE_HAS_WA="no"
219
+ IFS=',' read -ra ACTIVE_KEY_ARR <<< "$ACTIVE_KEYS"
220
+ for k in "${ACTIVE_KEY_ARR[@]}"; do
221
+ case "$k" in
222
+ PUB_WA_*) ACTIVE_HAS_WA="yes" ;;
223
+ esac
224
+ done
225
+
226
+ if [ "$ACTIVE_HAS_WA" = "yes" ]; then
227
+ # WebAuth case — must rewrite active to the K1.
228
+ NEW_ACTIVE_KEY="$MATCHED_OWNER_KEY"
229
+ ACTIVE_NEEDS_REWRITE="yes"
230
+ info "active permission is WebAuth-only (PUB_WA_...) — agent can't sign autonomously."
231
+ info "Will replace active with the K1 currently on owner: $MATCHED_OWNER_KEY"
232
+ elif [ -z "$ACTIVE_KEYS" ]; then
233
+ err "active permission has no keys. This is unusual — check manually:"
234
+ info " proton account $AGENT_ACCOUNT"
235
+ exit 1
236
+ else
237
+ # Active already has K1(s). Leave alone.
238
+ NEW_ACTIVE_KEY="$(printf '%s' "$ACTIVE_KEYS" | cut -d, -f1)"
239
+ ACTIVE_NEEDS_REWRITE="no"
240
+ info "active permission already has K1 key: $NEW_ACTIVE_KEY"
241
+ info "No change to active needed."
242
+ fi
243
+
244
+ # Verify the K1 we want on active is in the keychain (whether we're
245
+ # rewriting or leaving alone — either way, the agent needs to sign with
246
+ # it daily).
247
+ if ! printf '%s' "$KEYLIST" | grep -qF "\"publicKey\": \"$NEW_ACTIVE_KEY\""; then
248
+ err "Final active key ($NEW_ACTIVE_KEY) is not in the proton CLI keychain."
249
+ err "The agent would not be able to sign after this change. Aborting."
250
+ info "Load the matching PVT_K1_ key first: proton key:add"
251
+ exit 1
252
+ fi
253
+ ok "Final active key ($NEW_ACTIVE_KEY) is in the keychain."
254
+
255
+ # ── Step 3: Ask for human account ──────────────
256
+ step "3/6" "Your personal XPR account"
257
+
258
+ cat <<EOF
259
+
260
+ ${YELLOW}This account will control recovery of '$AGENT_ACCOUNT' forever.${NC}
261
+
262
+ It MUST be an account you fully control TODAY. Strongly recommended:
263
+ • KYC-verified (gives the agent +30 trust score via the claim system)
264
+ • WebAuth-secured (Face ID / fingerprint signing, key never on a server)
265
+ • Not an account you share with anyone
266
+ • Not the same account as the agent itself
267
+
268
+ Be CAREFUL: a typo here delegates owner to a nonexistent or someone
269
+ else's account, and you will lose control of '$AGENT_ACCOUNT' forever.
270
+
271
+ EOF
272
+
273
+ read -rp " Your personal XPR account name: " HUMAN_ACCOUNT
274
+ HUMAN_ACCOUNT="$(printf '%s' "$HUMAN_ACCOUNT" | tr -d '[:space:]')"
275
+
276
+ if [ -z "$HUMAN_ACCOUNT" ]; then
277
+ abort "No account name given. Aborting."
278
+ fi
279
+ if ! printf '%s' "$HUMAN_ACCOUNT" | grep -qE '^[.1-5a-z]{1,12}$'; then
280
+ abort "'$HUMAN_ACCOUNT' is not a valid XPR Network account name (1-12 chars from .12345a-z)."
281
+ fi
282
+ if [ "$HUMAN_ACCOUNT" = "$AGENT_ACCOUNT" ]; then
283
+ abort "The human account cannot be the same as the agent account. That defeats the entire purpose."
284
+ fi
285
+
286
+ info ""
287
+ info "Looking up '$HUMAN_ACCOUNT' on chain..."
288
+ if ! HUMAN_JSON=$(proton account "$HUMAN_ACCOUNT" --json 2>/dev/null); then
289
+ err "Account '$HUMAN_ACCOUNT' not found on chain."
290
+ info "Did you typo it? Verify at:"
291
+ info " https://explorer.xprnetwork.org/account/$HUMAN_ACCOUNT"
292
+ info ""
293
+ abort "Refusing to delegate owner to a nonexistent account."
294
+ fi
295
+
296
+ # Extract human account info for display
297
+ HUMAN_BALANCE=$(printf '%s' "$HUMAN_JSON" | node -e "
298
+ let s=''; process.stdin.on('data',d=>s+=d); process.stdin.on('end',()=>{
299
+ try {
300
+ const a = JSON.parse(s);
301
+ console.log(a.core_liquid_balance || '0 XPR');
302
+ } catch(e) { console.log('?'); }
303
+ });
304
+ " <<< "$HUMAN_JSON")
305
+ HUMAN_CREATED=$(printf '%s' "$HUMAN_JSON" | node -e "
306
+ let s=''; process.stdin.on('data',d=>s+=d); process.stdin.on('end',()=>{
307
+ try {
308
+ const a = JSON.parse(s);
309
+ console.log((a.created || '').slice(0,10) || '?');
310
+ } catch(e) { console.log('?'); }
311
+ });
312
+ " <<< "$HUMAN_JSON")
313
+ HUMAN_ACTIVE_KEYS=$(printf '%s' "$HUMAN_JSON" | node -e "
314
+ let s=''; process.stdin.on('data',d=>s+=d); process.stdin.on('end',()=>{
315
+ try {
316
+ const a = JSON.parse(s);
317
+ const active = (a.permissions||[]).find(p=>p.perm_name==='active');
318
+ const keys = (active?.required_auth?.keys||[]).map(k=>k.key);
319
+ console.log(keys.join(',') || '(no raw keys)');
320
+ } catch(e) { console.log('?'); }
321
+ });
322
+ " <<< "$HUMAN_JSON")
323
+
324
+ ok "Account exists"
325
+ info " Created: $HUMAN_CREATED"
326
+ info " XPR balance: $HUMAN_BALANCE"
327
+ info " active perm keys: $HUMAN_ACTIVE_KEYS"
328
+
329
+ # Warn if human account isn't WebAuth-secured (recommendation, not a block)
330
+ case "$HUMAN_ACTIVE_KEYS" in
331
+ *PUB_WA_*) info " ✓ active key is WebAuth (biometric) — good." ;;
332
+ *) warn " active key is NOT WebAuth-biometric. This is allowed but weaker." ;;
333
+ esac
334
+
335
+ # ── Step 4: Critical confirmation ──────────────
336
+ step "4/6" "${BOLD}${RED}CRITICAL CONFIRMATION${NC}"
337
+
338
+ cat <<EOF
339
+
340
+ Open this URL in your browser ${BOLD}right now${NC}:
341
+ ${BOLD}https://explorer.xprnetwork.org/account/$HUMAN_ACCOUNT${NC}
342
+
343
+ Visually verify on the explorer:
344
+ ☐ The account name is exactly '$HUMAN_ACCOUNT' (no typos)
345
+ ☐ The balance / activity look like YOUR account
346
+ ☐ You can sign transactions from this account today
347
+
348
+ If ANY of these are wrong, hit ${BOLD}Ctrl+C${NC} now and start over.
349
+
350
+ After this script completes, '$AGENT_ACCOUNT' is controlled by '$HUMAN_ACCOUNT'
351
+ forever — only '$HUMAN_ACCOUNT' can change it back.
352
+
353
+ EOF
354
+
355
+ read -rp " Type the agent account name '${AGENT_ACCOUNT}' to confirm target: " CONFIRM_AGENT
356
+ if [ "$CONFIRM_AGENT" != "$AGENT_ACCOUNT" ]; then
357
+ abort "Agent account didn't match. Aborting (no changes made)."
358
+ fi
359
+
360
+ read -rp " Type the human account name '${HUMAN_ACCOUNT}' to confirm controller: " CONFIRM_HUMAN
361
+ if [ "$CONFIRM_HUMAN" != "$HUMAN_ACCOUNT" ]; then
362
+ abort "Human account didn't match. Aborting (no changes made)."
363
+ fi
364
+
365
+ # ── Step 5: Show transaction plan ──────────────
366
+ step "5/6" "Transaction plan"
367
+
368
+ cat <<EOF
369
+
370
+ Will push ONE atomic transaction to '$AGENT_ACCOUNT' with the
371
+ following updateauth actions:
372
+
373
+ EOF
374
+
375
+ if [ "$ACTIVE_NEEDS_REWRITE" = "yes" ]; then
376
+ cat <<EOF
377
+ ${BOLD}action 1: active → K1${NC}
378
+ permission: active
379
+ parent: owner
380
+ threshold: 1
381
+ keys: [$NEW_ACTIVE_KEY]
382
+ accounts: []
383
+ (was: $ACTIVE_KEYS)
384
+ EOF
385
+ fi
386
+
387
+ cat <<EOF
388
+
389
+ ${BOLD}action $([ "$ACTIVE_NEEDS_REWRITE" = "yes" ] && echo 2 || echo 1): owner → human account${NC}
390
+ permission: owner
391
+ parent: (root)
392
+ threshold: 1
393
+ keys: []
394
+ accounts: [${HUMAN_ACCOUNT}@active]
395
+ (was raw key: $MATCHED_OWNER_KEY)
396
+
397
+ Signed by: ${AGENT_ACCOUNT}@owner (from your proton CLI keychain)
398
+
399
+ ${RED}${BOLD}This is irreversible from the agent side.${NC} After this lands,
400
+ only ${HUMAN_ACCOUNT}@active can change ${AGENT_ACCOUNT}'s permissions.
401
+
402
+ EOF
403
+
404
+ read -rp " Type ${BOLD}'yes I understand'${NC} to proceed (anything else aborts): " FINAL
405
+ if [ "$FINAL" != "yes I understand" ]; then
406
+ abort "Aborted at final confirmation. No changes made."
407
+ fi
408
+
409
+ # ── Step 6: Build and push transaction ─────────
410
+ step "6/6" "Pushing transaction..."
411
+
412
+ # Build the actions array
413
+ ACTIONS_JSON=""
414
+ if [ "$ACTIVE_NEEDS_REWRITE" = "yes" ]; then
415
+ ACTIONS_JSON="{\"account\":\"eosio\",\"name\":\"updateauth\",\"authorization\":[{\"actor\":\"${AGENT_ACCOUNT}\",\"permission\":\"owner\"}],\"data\":{\"account\":\"${AGENT_ACCOUNT}\",\"permission\":\"active\",\"parent\":\"owner\",\"auth\":{\"threshold\":1,\"keys\":[{\"key\":\"${NEW_ACTIVE_KEY}\",\"weight\":1}],\"accounts\":[],\"waits\":[]}}},"
416
+ fi
417
+ ACTIONS_JSON="${ACTIONS_JSON}{\"account\":\"eosio\",\"name\":\"updateauth\",\"authorization\":[{\"actor\":\"${AGENT_ACCOUNT}\",\"permission\":\"owner\"}],\"data\":{\"account\":\"${AGENT_ACCOUNT}\",\"permission\":\"owner\",\"parent\":\"\",\"auth\":{\"threshold\":1,\"keys\":[],\"accounts\":[{\"permission\":{\"actor\":\"${HUMAN_ACCOUNT}\",\"permission\":\"active\"},\"weight\":1}],\"waits\":[]}}}"
418
+
419
+ TX_JSON="{\"actions\":[${ACTIONS_JSON}]}"
420
+
421
+ # Push. Capture both stdout and stderr.
422
+ if ! TX_RESULT=$(proton transaction:push "$TX_JSON" 2>&1); then
423
+ err "Transaction failed:"
424
+ printf '%s\n' "$TX_RESULT"
425
+ err ""
426
+ err "No permission changes were made (EOSIO transactions are atomic)."
427
+ err "If this is recoverable, re-run the script. Otherwise check:"
428
+ info " proton account $AGENT_ACCOUNT"
429
+ exit 1
430
+ fi
431
+
432
+ # Extract tx id from the result (proton CLI prints it)
433
+ TX_ID=$(printf '%s' "$TX_RESULT" | grep -oE '[a-f0-9]{64}' | head -1)
434
+ if [ -n "$TX_ID" ]; then
435
+ ok "tx $TX_ID submitted"
436
+ else
437
+ warn "Transaction submitted but tx id not parsed from output."
438
+ info "Output: $TX_RESULT"
439
+ fi
440
+
441
+ # Wait a beat for the tx to land
442
+ sleep 3
443
+
444
+ # Re-fetch and verify end-state
445
+ info ""
446
+ info "Verifying final state..."
447
+ if ! POST_JSON=$(proton account "$AGENT_ACCOUNT" --json 2>/dev/null); then
448
+ err "Failed to re-fetch account state. Verify manually:"
449
+ info " proton account $AGENT_ACCOUNT"
450
+ info " https://explorer.xprnetwork.org/account/$AGENT_ACCOUNT"
451
+ exit 1
452
+ fi
453
+
454
+ POST_OWNER_KEYS=$(printf '%s' "$POST_JSON" | node -e "
455
+ let s=''; process.stdin.on('data',d=>s+=d); process.stdin.on('end',()=>{
456
+ try {
457
+ const a = JSON.parse(s);
458
+ const owner = (a.permissions||[]).find(p=>p.perm_name==='owner');
459
+ const keys = (owner?.required_auth?.keys||[]).map(k=>k.key);
460
+ console.log(keys.join(','));
461
+ } catch(e) { console.log(''); }
462
+ });
463
+ " <<< "$POST_JSON")
464
+ POST_OWNER_ACCOUNTS=$(printf '%s' "$POST_JSON" | node -e "
465
+ let s=''; process.stdin.on('data',d=>s+=d); process.stdin.on('end',()=>{
466
+ try {
467
+ const a = JSON.parse(s);
468
+ const owner = (a.permissions||[]).find(p=>p.perm_name==='owner');
469
+ const accts = (owner?.required_auth?.accounts||[]).map(x=>x.permission.actor+'@'+x.permission.permission);
470
+ console.log(accts.join(','));
471
+ } catch(e) { console.log(''); }
472
+ });
473
+ " <<< "$POST_JSON")
474
+
475
+ if [ -n "$POST_OWNER_KEYS" ]; then
476
+ err "owner still has raw keys after the change: $POST_OWNER_KEYS"
477
+ err "Pillar 2 is NOT in place. Investigate via the explorer."
478
+ exit 1
479
+ fi
480
+
481
+ if [ "$POST_OWNER_ACCOUNTS" != "${HUMAN_ACCOUNT}@active" ]; then
482
+ err "owner is not what we expected. Got: $POST_OWNER_ACCOUNTS"
483
+ err "Investigate via the explorer."
484
+ exit 1
485
+ fi
486
+
487
+ ok "owner now controlled by: ${HUMAN_ACCOUNT}@active (no raw keys)"
488
+ ok "Pillar 2 is in place."
489
+
490
+ cat <<EOF
491
+
492
+ ${GREEN}${BOLD}Done.${NC} '$AGENT_ACCOUNT' is secured.
493
+
494
+ • Daily signing: still works (active key is in the proton CLI keychain).
495
+ • Recovery: signed from ${HUMAN_ACCOUNT}@active if you ever need to rotate active.
496
+
497
+ Verify visually:
498
+ https://explorer.xprnetwork.org/account/$AGENT_ACCOUNT
499
+
500
+ Next steps (optional):
501
+ • Dry-run the recovery: see docs/SECURITY.md → "Optional: test recovery".
502
+ • Claim the agent via your KYC'd human account for +30 trust score.
503
+
504
+ EOF
@@ -208,13 +208,14 @@ When a job involves token swaps, OTC deals, or any financial trade:
208
208
  ## Safety Rules
209
209
 
210
210
  1. **Never reveal private keys** — Your blockchain key lives in the proton CLI's encrypted keychain (loaded once via `proton key:add`) and never enters this process's memory. Do not attempt to read it, dump it, or print it. Recommend operators use a dedicated agent account, not their personal account.
211
- 2. **Always verify before accepting** - Read job details thoroughly before committing
212
- 3. **Always provide evidence** - When delivering or disputing, include evidence URIs
213
- 4. **Respect confirmation gates** - High-risk actions (registration, funding, disputes) require confirmation
214
- 5. **Monitor your reputation** - A declining trust score needs investigation
215
- 6. **Don't over-commit** - Only accept jobs you can realistically complete
216
- 7. **Always check market price before trading** - Never execute a trade without verifying current rates
217
- 8. **Never sell tokens below market rate** - Protect your holdings from bad deals
211
+ 2. **Two-pillar security model** — Your account should be secured at two layers: (a) **active key** lives in the proton CLI keychain — you sign every transaction via shell-out (Pillar 1). (b) **owner permission** should point at the operator's separate human XPR account, not a raw key (Pillar 2). If the operator hasn't done Pillar 2, recommend they run `./setup-security.sh` (standalone scaffold) or `npx @xpr-agents/openclaw xpr-agents-setup-security --account <name>` (harness). The boot-time security warning from start.sh will flag this. See `docs/SECURITY.md`.
212
+ 3. **Always verify before accepting** - Read job details thoroughly before committing
213
+ 4. **Always provide evidence** - When delivering or disputing, include evidence URIs
214
+ 5. **Respect confirmation gates** - High-risk actions (registration, funding, disputes) require confirmation
215
+ 6. **Monitor your reputation** - A declining trust score needs investigation
216
+ 7. **Don't over-commit** - Only accept jobs you can realistically complete
217
+ 8. **Always check market price before trading** - Never execute a trade without verifying current rates
218
+ 9. **Never sell tokens below market rate** - Protect your holdings from bad deals
218
219
 
219
220
  ## Tool Quick Reference
220
221