@thirdfy/agent-cli 0.1.10 → 0.1.12

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/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@ All notable changes to `@thirdfy/agent-cli` are documented here. The format is b
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.1.11] - 2026-04-16
8
+
9
+ **npm:** [`@thirdfy/agent-cli@0.1.11`](https://www.npmjs.com/package/@thirdfy/agent-cli/v/0.1.11)
10
+ **Git tag:** `v0.1.11`
11
+
12
+ ### Added
13
+
14
+ - **Self identity bootstrap during login:** `thirdfy-agent login` can now auto-bootstrap scoped self identity when `--agent-key` is provided, and persist the returned `agentApiKey` without manual env injection.
15
+ - **Custody mode profile contract:** Added persisted `custodyMode` (`managed` or `external`) across `profile init`, `profile use`, and `whoami`/config UX.
16
+ - **Self custody validation E2E:** Added `e2e:self:managed-custody-validation` and wired it into production certification flow.
17
+
7
18
  ## [0.1.9] - 2026-04-08
8
19
 
9
20
  **npm:** [`@thirdfy/agent-cli@0.1.9`](https://www.npmjs.com/package/@thirdfy/agent-cli/v/0.1.9)
package/README.md CHANGED
@@ -81,9 +81,13 @@ Execution-only workflows can run with only `AGENT_API_KEY` after onboarding is c
81
81
  # Store auth defaults once
82
82
  thirdfy-agent login --auth-token "$THIRDFY_AUTH_TOKEN" --agent-api-key "$AGENT_API_KEY" --json
83
83
 
84
+ # Bootstrap scoped self identity + custody mode in one step
85
+ thirdfy-agent login --auth-token "$THIRDFY_AUTH_TOKEN" --agent-key "0xYOUR_AGENT_KEY" --run-mode self --custody-mode external --wallet-address "0xYOUR_AGENT_KEY" --json
86
+
84
87
  # Inspect config
85
88
  thirdfy-agent whoami --json
86
89
  thirdfy-agent config set --key runMode --value self --json
90
+ thirdfy-agent config set --key custodyMode --value external --json
87
91
 
88
92
  # Local-only logout (default)
89
93
  thirdfy-agent logout --json
@@ -136,6 +140,9 @@ Auth expectations by mode:
136
140
  - `self`: requires execution identity (`agentApiKey`) today; keyless self lane is blocked with deterministic `SELF_OPEN_DISABLED`.
137
141
  - `hybrid`: requires execution identity and governed mirror semantics.
138
142
  - `thirdfy`: requires execution identity and delegated governance path.
143
+ - custody mode defaults are profile-aware:
144
+ - `managed` default for `thirdfy`
145
+ - `external` default for `self` and `hybrid`
139
146
 
140
147
  Selection precedence:
141
148
 
@@ -344,6 +351,7 @@ thirdfy-agent managed run --agent-api-key "$AGENT_API_KEY" --run-mode thirdfy --
344
351
  npm run validate:cli-contract-schemas
345
352
  npm test
346
353
  npm run smoke:cli
354
+ npm run e2e:self:managed-custody-validation
347
355
  npm run e2e:managed:delegation:parity
348
356
  ```
349
357
 
@@ -24,6 +24,7 @@ const BITFINEX_REQUIRED_SCOPE_PERMISSIONS = {
24
24
  wallets: { read: 1, write: 1 },
25
25
  };
26
26
  const RUN_MODES = ['thirdfy', 'self', 'hybrid'];
27
+ const CUSTODY_MODES = ['managed', 'external'];
27
28
  const EFFECT_CHECK_MODES = ['strict', 'relaxed', 'off'];
28
29
  const PROFILE_DEFAULTS = {
29
30
  personal: 'self',
@@ -94,6 +95,7 @@ async function main() {
94
95
  const profileState = resolveProfileState(globalFlags);
95
96
  context.profile = profileState.profile;
96
97
  context.runMode = profileState.runMode;
98
+ context.custodyMode = profileState.custodyMode;
97
99
  context.profileConfigPath = profileState.configPath;
98
100
 
99
101
  if (globalFlags.version) {
@@ -212,6 +214,14 @@ async function main() {
212
214
  case 'developer bootstrap':
213
215
  await commandDeveloperBootstrap(context, subFlags, capabilities);
214
216
  return;
217
+ case 'bootstrap begin':
218
+ case 'onboarding begin':
219
+ await commandBootstrapBegin(context, subFlags, capabilities);
220
+ return;
221
+ case 'bootstrap complete':
222
+ case 'onboarding complete':
223
+ await commandBootstrapComplete(context, subFlags, capabilities);
224
+ return;
215
225
  default:
216
226
  break;
217
227
  }
@@ -1089,11 +1099,11 @@ async function commandDeveloperBootstrap(ctx, flags, capabilities) {
1089
1099
  if (!challengeId) {
1090
1100
  throw new Error('Missing --challenge-id for wallet bootstrap. Run `agent auth challenge` first.');
1091
1101
  }
1092
- const verify = await apiPost(ctx, '/api/v1/agent/onboarding/wallet/verify', {
1102
+ const verify = await verifyOwnerWalletChallenge(ctx, {
1103
+ ...flags,
1093
1104
  challengeId,
1094
1105
  signature,
1095
1106
  agentKey,
1096
- ...(flags.walletAddress ? { walletAddress: String(flags.walletAddress).trim() } : {}),
1097
1107
  });
1098
1108
  ownerSessionToken = String(verify?.ownerSessionToken || verify?.data?.ownerSessionToken || '').trim();
1099
1109
  usedWalletFlow = true;
@@ -1127,23 +1137,30 @@ async function commandDeveloperBootstrap(ctx, flags, capabilities) {
1127
1137
  },
1128
1138
  };
1129
1139
  persistProfileConfig(next);
1130
- printEnvelope({
1131
- success: true,
1132
- code: 'DEVELOPER_BOOTSTRAP_OK',
1133
- message: 'Developer bootstrap completed',
1134
- data: {
1135
- registration: registerResponse,
1136
- persisted: {
1137
- ownerSessionTokenSaved: Boolean(ownerSessionToken),
1138
- source: usedWalletFlow ? 'wallet-verify-bootstrap' : 'bootstrap',
1139
- configPath: getProfileConfigPath(),
1140
+ if (!flags.__suppressOutput) {
1141
+ printEnvelope({
1142
+ success: true,
1143
+ code: 'DEVELOPER_BOOTSTRAP_OK',
1144
+ message: 'Developer bootstrap completed',
1145
+ data: {
1146
+ registration: registerResponse,
1147
+ persisted: {
1148
+ ownerSessionTokenSaved: Boolean(ownerSessionToken),
1149
+ source: usedWalletFlow ? 'wallet-verify-bootstrap' : 'bootstrap',
1150
+ configPath: getProfileConfigPath(),
1151
+ },
1140
1152
  },
1141
- },
1142
- meta: {
1143
- apiBase: ctx.apiBase,
1144
- capabilitiesVersion: capabilities?.contractVersion || null,
1145
- },
1146
- });
1153
+ meta: {
1154
+ apiBase: ctx.apiBase,
1155
+ capabilitiesVersion: capabilities?.contractVersion || null,
1156
+ },
1157
+ });
1158
+ }
1159
+ return {
1160
+ registration: registerResponse,
1161
+ ownerSessionTokenSaved: Boolean(ownerSessionToken),
1162
+ source: usedWalletFlow ? 'wallet-verify-bootstrap' : 'bootstrap',
1163
+ };
1147
1164
  }
1148
1165
 
1149
1166
  async function commandAgentKeyRotate(ctx, flags, capabilities) {
@@ -1186,14 +1203,21 @@ async function commandAgentKeyRevoke(ctx, flags, capabilities) {
1186
1203
  });
1187
1204
  }
1188
1205
 
1189
- async function commandAgentAuthChallenge(ctx, flags, capabilities) {
1206
+ function buildWalletChallengePayload(flags) {
1190
1207
  const payload = {
1191
1208
  agentKey: requireFlag(flags, 'agentKey', 'Missing --agent-key'),
1192
1209
  chainId: Number(flags.chainId || 8453),
1193
1210
  };
1194
1211
  if (flags.walletAddress) payload.walletAddress = String(flags.walletAddress).trim();
1212
+ return payload;
1213
+ }
1195
1214
 
1196
- const response = await apiPost(ctx, '/api/v1/agent/onboarding/wallet/challenge', payload);
1215
+ async function commandAgentAuthChallenge(ctx, flags, capabilities) {
1216
+ const response = await apiPost(
1217
+ ctx,
1218
+ '/api/v1/agent/onboarding/wallet/challenge',
1219
+ buildWalletChallengePayload(flags)
1220
+ );
1197
1221
  printEnvelope({
1198
1222
  success: true,
1199
1223
  code: 'OWNER_CHALLENGE_CREATED',
@@ -1207,21 +1231,186 @@ async function commandAgentAuthChallenge(ctx, flags, capabilities) {
1207
1231
  }
1208
1232
 
1209
1233
  async function commandAgentAuthVerify(ctx, flags, capabilities) {
1234
+ const response = await verifyOwnerWalletChallenge(ctx, flags);
1235
+ printEnvelope({
1236
+ success: true,
1237
+ code: 'OWNER_VERIFIED',
1238
+ message: 'Wallet challenge verified',
1239
+ data: response,
1240
+ meta: {
1241
+ apiBase: ctx.apiBase,
1242
+ capabilitiesVersion: capabilities?.contractVersion || null,
1243
+ },
1244
+ });
1245
+ }
1246
+
1247
+ async function verifyOwnerWalletChallenge(ctx, flags) {
1210
1248
  const payload = {
1211
1249
  challengeId: requireFlag(flags, 'challengeId', 'Missing --challenge-id'),
1212
1250
  signature: requireFlag(flags, 'signature', 'Missing --signature'),
1213
1251
  agentKey: requireFlag(flags, 'agentKey', 'Missing --agent-key'),
1214
1252
  };
1215
1253
  if (flags.walletAddress) payload.walletAddress = String(flags.walletAddress).trim();
1254
+ return apiPost(ctx, '/api/v1/agent/onboarding/wallet/verify', payload);
1255
+ }
1216
1256
 
1217
- const response = await apiPost(ctx, '/api/v1/agent/onboarding/wallet/verify', payload);
1257
+ function resolveMcpBase(flags) {
1258
+ return String(flags.mcpBase || process.env.THIRDFY_MCP_BASE_URL || 'https://mcp.thirdfy.com').trim().replace(/\/+$/, '');
1259
+ }
1260
+
1261
+ async function issueBootstrapToken(ctx, flags, payload) {
1262
+ const mcpBase = resolveMcpBase(flags);
1263
+ const controller = new AbortController();
1264
+ const timeout = setTimeout(() => controller.abort(), Number(ctx.timeoutMs || DEFAULT_TIMEOUT_MS));
1265
+ let response;
1266
+ let text = '';
1267
+ try {
1268
+ response = await fetch(`${mcpBase}/auth/bootstrap/issue`, {
1269
+ method: 'POST',
1270
+ headers: {
1271
+ 'content-type': 'application/json',
1272
+ 'x-cli-version': CLI_VERSION,
1273
+ },
1274
+ body: JSON.stringify(payload),
1275
+ signal: controller.signal,
1276
+ });
1277
+ text = await response.text();
1278
+ } catch (error) {
1279
+ if (error?.name === 'AbortError') {
1280
+ throw createCliError('TIMEOUT', `Request timed out after ${ctx.timeoutMs}ms`, {
1281
+ routes: ['/auth/bootstrap/issue'],
1282
+ });
1283
+ }
1284
+ throw error;
1285
+ } finally {
1286
+ clearTimeout(timeout);
1287
+ }
1288
+ const data = text ? safeJsonParse(text) : {};
1289
+ if (!response.ok) {
1290
+ const message = data?.error || data?.message || 'Bootstrap token issuance failed';
1291
+ throw createCliError('BOOTSTRAP_TOKEN_ISSUE_FAILED', message, data);
1292
+ }
1293
+ return {
1294
+ mcpBase,
1295
+ data,
1296
+ };
1297
+ }
1298
+
1299
+ async function commandBootstrapBegin(ctx, flags, capabilities) {
1300
+ const challenge = await apiPost(
1301
+ ctx,
1302
+ '/api/v1/agent/onboarding/wallet/challenge',
1303
+ buildWalletChallengePayload(flags)
1304
+ );
1218
1305
  printEnvelope({
1219
1306
  success: true,
1220
- code: 'OWNER_VERIFIED',
1221
- message: 'Wallet challenge verified',
1222
- data: response,
1307
+ code: 'BOOTSTRAP_BEGIN_OK',
1308
+ message: 'Bootstrap challenge created',
1309
+ data: {
1310
+ challenge,
1311
+ nextStep:
1312
+ 'Run `thirdfy-agent bootstrap complete --agent-key <key> --challenge-id <id> --signature <hex> [--name <agent-name>]`',
1313
+ },
1314
+ meta: {
1315
+ apiBase: ctx.apiBase,
1316
+ capabilitiesVersion: capabilities?.contractVersion || null,
1317
+ },
1318
+ });
1319
+ }
1320
+
1321
+ async function commandBootstrapComplete(ctx, flags, capabilities) {
1322
+ const agentKey = requireFlag(flags, 'agentKey', 'Missing --agent-key');
1323
+ const lane = normalizeRunMode(flags.lane || flags.runMode || 'self', flags.lane ? '--lane' : '--run-mode');
1324
+ const proofType = String(flags.proofType || '').trim() || 'session_token';
1325
+ if (proofType !== 'session_token' && proofType !== 'wallet_signature') {
1326
+ throw new Error('Invalid --proof-type. Supported values: session_token (preferred) or wallet_signature (challenge exchange only).');
1327
+ }
1328
+ let ownerSessionToken = String(flags.ownerSessionToken || process.env.THIRDFY_OWNER_SESSION_TOKEN || '').trim();
1329
+ const authToken = String(flags.authToken || process.env.THIRDFY_AUTH_TOKEN || '').trim();
1330
+ let challengeId = String(flags.challengeId || '').trim();
1331
+ let signature = String(flags.signature || '').trim();
1332
+ if (proofType === 'wallet_signature' && (!challengeId || !signature)) {
1333
+ throw new Error('wallet_signature bootstrap is not supported directly. Provide --owner-session-token or --auth-token.');
1334
+ }
1335
+ if (!ownerSessionToken && !authToken && challengeId && signature) {
1336
+ const verify = await verifyOwnerWalletChallenge(ctx, {
1337
+ ...flags,
1338
+ agentKey,
1339
+ challengeId,
1340
+ signature,
1341
+ });
1342
+ ownerSessionToken = String(verify?.ownerSessionToken || verify?.data?.ownerSessionToken || '').trim();
1343
+ if (!ownerSessionToken) {
1344
+ throw new Error('Wallet verify did not return owner session token required for bootstrap registration.');
1345
+ }
1346
+ }
1347
+ if (ownerSessionToken || authToken) {
1348
+ // Avoid replaying a single-use challenge across bootstrap and register calls.
1349
+ challengeId = '';
1350
+ signature = '';
1351
+ }
1352
+ if (!ownerSessionToken && !authToken) {
1353
+ throw new Error(
1354
+ 'Bootstrap currently supports session_token proof only. Provide --owner-session-token/--auth-token, or --challenge-id + --signature to exchange into a session token.'
1355
+ );
1356
+ }
1357
+
1358
+ const bootstrapPayload = {
1359
+ sub: agentKey,
1360
+ lane,
1361
+ scope: ['onboarding:bootstrap'],
1362
+ proof_type: 'session_token',
1363
+ ...(ownerSessionToken ? { ownerSessionToken } : {}),
1364
+ ...(authToken ? { authToken } : {}),
1365
+ agentKey,
1366
+ };
1367
+ const issued = await issueBootstrapToken(ctx, flags, bootstrapPayload);
1368
+ const bootstrapToken = String(issued.data?.token || '').trim();
1369
+
1370
+ const config = loadProfileConfig();
1371
+ const next = {
1372
+ ...config,
1373
+ auth: {
1374
+ ...(config.auth && typeof config.auth === 'object' ? config.auth : {}),
1375
+ bootstrapToken,
1376
+ bootstrapTokenIssuedAt: new Date().toISOString(),
1377
+ bootstrapLane: lane,
1378
+ },
1379
+ };
1380
+ persistProfileConfig(next);
1381
+
1382
+ let registration = null;
1383
+ if (flags.name) {
1384
+ registration = await commandDeveloperBootstrap(
1385
+ ctx,
1386
+ {
1387
+ ...flags,
1388
+ agentKey,
1389
+ name: String(flags.name).trim(),
1390
+ challengeId,
1391
+ signature,
1392
+ ...(ownerSessionToken ? { ownerSessionToken } : {}),
1393
+ __suppressOutput: true,
1394
+ },
1395
+ capabilities
1396
+ );
1397
+ }
1398
+
1399
+ printEnvelope({
1400
+ success: true,
1401
+ code: 'BOOTSTRAP_COMPLETE_OK',
1402
+ message: 'Bootstrap token issued',
1403
+ data: {
1404
+ bootstrap: issued.data,
1405
+ registration,
1406
+ persisted: {
1407
+ configPath: getProfileConfigPath(),
1408
+ hasBootstrapToken: Boolean(bootstrapToken),
1409
+ },
1410
+ },
1223
1411
  meta: {
1224
1412
  apiBase: ctx.apiBase,
1413
+ mcpBase: issued.mcpBase,
1225
1414
  capabilitiesVersion: capabilities?.contractVersion || null,
1226
1415
  },
1227
1416
  });
@@ -1401,13 +1590,16 @@ async function commandProfileInit(ctx, flags) {
1401
1590
  const current = loadProfileConfig();
1402
1591
  const profile = normalizeProfileName(flags.profile || flags.name || 'personal');
1403
1592
  const runMode = normalizeRunMode(flags.runMode || PROFILE_DEFAULTS[profile]);
1593
+ const custodyMode = normalizeCustodyMode(flags.custodyMode, runMode);
1404
1594
  persistProfileConfig({
1405
1595
  ...current,
1406
1596
  profile,
1407
1597
  runMode,
1598
+ custodyMode,
1408
1599
  });
1409
1600
  ctx.profile = profile;
1410
1601
  ctx.runMode = runMode;
1602
+ ctx.custodyMode = custodyMode;
1411
1603
  printEnvelope({
1412
1604
  success: true,
1413
1605
  code: 'PROFILE_INITIALIZED',
@@ -1415,6 +1607,7 @@ async function commandProfileInit(ctx, flags) {
1415
1607
  data: {
1416
1608
  profile,
1417
1609
  runMode,
1610
+ custodyMode,
1418
1611
  configPath: getProfileConfigPath(),
1419
1612
  },
1420
1613
  meta: { apiBase: ctx.apiBase },
@@ -1426,13 +1619,16 @@ async function commandProfileUse(ctx, flags) {
1426
1619
  const profile = normalizeProfileName(flags.profile || flags.name || current.profile || 'personal');
1427
1620
  // `profile use` intentionally re-applies profile defaults unless caller pins a mode.
1428
1621
  const runMode = normalizeRunMode(flags.runMode || PROFILE_DEFAULTS[profile]);
1622
+ const custodyMode = normalizeCustodyMode(flags.custodyMode || current.custodyMode, runMode);
1429
1623
  persistProfileConfig({
1430
1624
  ...current,
1431
1625
  profile,
1432
1626
  runMode,
1627
+ custodyMode,
1433
1628
  });
1434
1629
  ctx.profile = profile;
1435
1630
  ctx.runMode = runMode;
1631
+ ctx.custodyMode = custodyMode;
1436
1632
  printEnvelope({
1437
1633
  success: true,
1438
1634
  code: 'PROFILE_UPDATED',
@@ -1440,6 +1636,7 @@ async function commandProfileUse(ctx, flags) {
1440
1636
  data: {
1441
1637
  profile,
1442
1638
  runMode,
1639
+ custodyMode,
1443
1640
  defaults: PROFILE_DEFAULTS,
1444
1641
  configPath: getProfileConfigPath(),
1445
1642
  },
@@ -1456,6 +1653,7 @@ async function commandProfileShow(ctx, _flags) {
1456
1653
  data: {
1457
1654
  profile: ctx.profile || config.profile || 'personal',
1458
1655
  runMode: ctx.runMode || config.runMode || 'thirdfy',
1656
+ custodyMode: ctx.custodyMode || config.custodyMode || defaultCustodyModeForRunMode(ctx.runMode || config.runMode || 'thirdfy'),
1459
1657
  profileDefaults: PROFILE_DEFAULTS,
1460
1658
  configPath: getProfileConfigPath(),
1461
1659
  config,
@@ -1478,6 +1676,10 @@ async function commandConfigSet(ctx, flags) {
1478
1676
  if (canonicalKey === 'runmode' && hasValue) {
1479
1677
  normalizeRunMode(value);
1480
1678
  }
1679
+ if (canonicalKey === 'custodymode' && hasValue) {
1680
+ const runModeHint = normalizeRunMode(flags.runMode || loadProfileConfig().runMode || 'thirdfy');
1681
+ normalizeCustodyMode(value, runModeHint);
1682
+ }
1481
1683
 
1482
1684
  const current = loadProfileConfig();
1483
1685
  const next = setNestedConfigValue(current, key, value);
@@ -1499,6 +1701,11 @@ async function commandLogin(ctx, flags) {
1499
1701
  const current = loadProfileConfig();
1500
1702
  const authToken = String(flags.authToken || process.env.THIRDFY_AUTH_TOKEN || '').trim();
1501
1703
  let ownerSessionToken = String(flags.ownerSessionToken || process.env.THIRDFY_OWNER_SESSION_TOKEN || '').trim();
1704
+ const loginRunMode = normalizeRunMode(flags.runMode || current.runMode || process.env.THIRDFY_RUN_MODE || 'thirdfy');
1705
+ const custodyMode = normalizeCustodyMode(
1706
+ flags.custodyMode || current.custodyMode || process.env.THIRDFY_CUSTODY_MODE,
1707
+ loginRunMode
1708
+ );
1502
1709
  let loginSource = 'token';
1503
1710
 
1504
1711
  if (!authToken && !ownerSessionToken) {
@@ -1529,8 +1736,40 @@ async function commandLogin(ctx, flags) {
1529
1736
  );
1530
1737
  }
1531
1738
 
1739
+ let bootstrapAgentApiKey = '';
1740
+ let bootstrapPerformed = false;
1741
+ const requestedAgentApiKey = String(flags.agentApiKey || '').trim();
1742
+ const bootstrapAgentKey = String(flags.agentKey || '').trim();
1743
+ if (!requestedAgentApiKey && bootstrapAgentKey) {
1744
+ const bootstrapHeaders = ownerSessionToken
1745
+ ? { 'x-owner-session-token': ownerSessionToken }
1746
+ : authToken
1747
+ ? { Authorization: `Bearer ${authToken}` }
1748
+ : null;
1749
+ if (bootstrapHeaders) {
1750
+ const bootstrapResponse = await requestWithRouteFallback(ctx, {
1751
+ method: 'POST',
1752
+ routes: ['/api/v1/agent/onboarding/me/bootstrap-self'],
1753
+ headers: bootstrapHeaders,
1754
+ body: {
1755
+ agentKey: bootstrapAgentKey,
1756
+ runMode: loginRunMode,
1757
+ custodyMode,
1758
+ ...(flags.walletAddress ? { walletAddress: String(flags.walletAddress).trim() } : {}),
1759
+ rotate: parseBooleanFlag(flags.rotateAgentKey, false),
1760
+ },
1761
+ });
1762
+ bootstrapAgentApiKey = String(
1763
+ bootstrapResponse?.agentApiKey || bootstrapResponse?.data?.agentApiKey || ''
1764
+ ).trim();
1765
+ bootstrapPerformed = true;
1766
+ }
1767
+ }
1768
+
1532
1769
  const next = {
1533
1770
  ...current,
1771
+ runMode: loginRunMode,
1772
+ custodyMode,
1534
1773
  auth: {
1535
1774
  ...(current.auth && typeof current.auth === 'object' ? current.auth : {}),
1536
1775
  lastLoginAt: new Date().toISOString(),
@@ -1547,7 +1786,12 @@ async function commandLogin(ctx, flags) {
1547
1786
  if (flags.agentApiKey) {
1548
1787
  next.agent = {
1549
1788
  ...(current.agent && typeof current.agent === 'object' ? current.agent : {}),
1550
- apiKey: String(flags.agentApiKey).trim(),
1789
+ apiKey: requestedAgentApiKey,
1790
+ };
1791
+ } else if (bootstrapAgentApiKey) {
1792
+ next.agent = {
1793
+ ...(current.agent && typeof current.agent === 'object' ? current.agent : {}),
1794
+ apiKey: bootstrapAgentApiKey,
1551
1795
  };
1552
1796
  }
1553
1797
  persistProfileConfig(next);
@@ -1558,7 +1802,10 @@ async function commandLogin(ctx, flags) {
1558
1802
  data: {
1559
1803
  hasAuthToken: Boolean(authToken),
1560
1804
  hasOwnerSessionToken: Boolean(ownerSessionToken),
1561
- hasAgentApiKey: Boolean(flags.agentApiKey),
1805
+ hasAgentApiKey: Boolean(requestedAgentApiKey || bootstrapAgentApiKey),
1806
+ bootstrapPerformed,
1807
+ runMode: loginRunMode,
1808
+ custodyMode,
1562
1809
  configPath: getProfileConfigPath(),
1563
1810
  },
1564
1811
  meta: { apiBase: ctx.apiBase },
@@ -1622,9 +1869,14 @@ function resolveProfileState(flags) {
1622
1869
  PROFILE_DEFAULTS[profile] ||
1623
1870
  'thirdfy'
1624
1871
  );
1872
+ const custodyMode = normalizeCustodyMode(
1873
+ flags.custodyMode || process.env.THIRDFY_CUSTODY_MODE || persisted.custodyMode,
1874
+ runMode
1875
+ );
1625
1876
  return {
1626
1877
  profile,
1627
1878
  runMode,
1879
+ custodyMode,
1628
1880
  configPath: getProfileConfigPath(),
1629
1881
  };
1630
1882
  }
@@ -1635,11 +1887,23 @@ function normalizeProfileName(value) {
1635
1887
  throw new Error(`Unsupported profile "${value}". Supported: ${Object.keys(PROFILE_DEFAULTS).join(', ')}`);
1636
1888
  }
1637
1889
 
1638
- function normalizeRunMode(value) {
1890
+ function normalizeRunMode(value, flagName = '--run-mode') {
1639
1891
  const normalized = String(value || '').trim().toLowerCase();
1640
1892
  if (!normalized) return 'thirdfy';
1641
1893
  if (RUN_MODES.includes(normalized)) return normalized;
1642
- throw new Error(`Unsupported --run-mode "${value}". Supported: ${RUN_MODES.join(', ')}`);
1894
+ throw new Error(`Unsupported ${flagName} "${value}". Supported: ${RUN_MODES.join(', ')}`);
1895
+ }
1896
+
1897
+ function defaultCustodyModeForRunMode(runMode) {
1898
+ return runMode === 'thirdfy' ? 'managed' : 'external';
1899
+ }
1900
+
1901
+ function normalizeCustodyMode(value, runModeHint = 'thirdfy') {
1902
+ const fallback = defaultCustodyModeForRunMode(runModeHint);
1903
+ const normalized = String(value || '').trim().toLowerCase();
1904
+ if (!normalized) return fallback;
1905
+ if (CUSTODY_MODES.includes(normalized)) return normalized;
1906
+ throw new Error(`Unsupported custody mode "${value}". Supported: ${CUSTODY_MODES.join(', ')}`);
1643
1907
  }
1644
1908
 
1645
1909
  function normalizeManagedRunMode(value) {
@@ -2700,11 +2964,11 @@ function printHelp() {
2700
2964
  thirdfy-agent ${CLI_VERSION}
2701
2965
 
2702
2966
  Core commands:
2703
- thirdfy-agent profile init [--profile personal|builder|network] [--run-mode thirdfy|self|hybrid] [--json]
2704
- thirdfy-agent profile use --profile <name> [--run-mode thirdfy|self|hybrid] [--json]
2967
+ thirdfy-agent profile init [--profile personal|builder|network] [--run-mode thirdfy|self|hybrid] [--custody-mode managed|external] [--json]
2968
+ thirdfy-agent profile use --profile <name> [--run-mode thirdfy|self|hybrid] [--custody-mode managed|external] [--json]
2705
2969
  thirdfy-agent profile show [--json]
2706
2970
  thirdfy-agent config set --key <path> --value <value> [--json]
2707
- thirdfy-agent login [--auth-token <token> | --owner-session-token <token>] [--agent-api-key <key>] [--json]
2971
+ thirdfy-agent login [--auth-token <token> | --owner-session-token <token>] [--agent-api-key <key>] [--agent-key <key>] [--run-mode <mode>] [--custody-mode managed|external] [--wallet-address <addr>] [--json]
2708
2972
  thirdfy-agent logout [--all] [--clear-agent-key] [--json]
2709
2973
  thirdfy-agent whoami [--json]
2710
2974
  thirdfy-agent catalogs list [--json]
@@ -2726,6 +2990,10 @@ Core commands:
2726
2990
  thirdfy-agent credentials status --auth-token <token> [--venue <id>] [--json]
2727
2991
  thirdfy-agent agent auth challenge --agent-key <key> [--wallet-address <addr>] [--chain-id <id>] [--json]
2728
2992
  thirdfy-agent agent auth verify --challenge-id <id> --signature <hex> --agent-key <key> [--wallet-address <addr>] [--json]
2993
+ thirdfy-agent bootstrap begin --agent-key <key> [--wallet-address <addr>] [--chain-id <id>] [--json]
2994
+ thirdfy-agent bootstrap complete --agent-key <key> [--proof-type session_token] [--lane self|hybrid|thirdfy] [--owner-session-token <token> | --auth-token <token> | --challenge-id <id> --signature <hex>] [--name <agent-name>] [--mcp-base <url>] [--json]
2995
+ thirdfy-agent onboarding begin ... # alias of bootstrap begin
2996
+ thirdfy-agent onboarding complete ... # alias of bootstrap complete
2729
2997
  thirdfy-agent developer bootstrap --agent-key <key> --name <name> [--owner-session-token <token> | --challenge-id <id> --signature <hex>] [--json]
2730
2998
  thirdfy-agent agent register --agent-key <key> --name <name> [--auth-token <token> | --owner-session-token <token>] [--json]
2731
2999
  thirdfy-agent agent key rotate [--agent-key <key>] [--auth-token <token> | --owner-session-token <token>] [--json]
@@ -2743,7 +3011,9 @@ Core commands:
2743
3011
 
2744
3012
  Global flags:
2745
3013
  --api-base <url> default: https://api.thirdfy.com
3014
+ --mcp-base <url> default: https://mcp.thirdfy.com (bootstrap token issuance)
2746
3015
  --run-mode <mode> thirdfy | self | hybrid
3016
+ --custody-mode <mode> managed | external
2747
3017
  --profile <name> personal | builder | network
2748
3018
  --env <file> load env vars from file
2749
3019
  --timeout <ms> request timeout
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thirdfy/agent-cli",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Thirdfy Agent CLI for onboarding, governance preflight, execute-intent, and status polling.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,6 +27,7 @@
27
27
  "e2e:delegation:lifecycle": "node ./scripts/e2e/delegation-lifecycle.mjs",
28
28
  "e2e:delegation:commands": "node ./scripts/e2e/delegation-command-parity.mjs",
29
29
  "e2e:self:delegation:parity": "node ./scripts/e2e/self-delegation-parity.mjs",
30
+ "e2e:self:managed-custody-validation": "node ./scripts/e2e/self-managed-custody-validation.mjs",
30
31
  "e2e:managed:delegation:parity": "node ./scripts/e2e/managed-custodial-lifecycle.mjs",
31
32
  "e2e:delegation:prod:certify": "node ./scripts/e2e/prod-delegation-certify.mjs",
32
33
  "e2e:report:last": "node ./scripts/e2e/report-last.mjs",
@@ -53,6 +54,7 @@
53
54
  },
54
55
  "homepage": "https://github.com/thirdfy/agent-cli#readme",
55
56
  "dependencies": {
57
+ "@open-wallet-standard/core": "^1.3.0",
56
58
  "ethers": "^6.16.0"
57
59
  }
58
60
  }