@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 +11 -0
- package/README.md +8 -0
- package/bin/thirdfy-agent.mjs +301 -31
- package/package.json +3 -1
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
|
|
package/bin/thirdfy-agent.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
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
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: '
|
|
1221
|
-
message: '
|
|
1222
|
-
data:
|
|
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:
|
|
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(
|
|
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
|
|
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.
|
|
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
|
}
|