@thirdfy/agent-cli 0.2.30 → 0.2.31
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 +12 -0
- package/README.md +3 -3
- package/package.json +2 -2
- package/src/cli/help.mjs +1 -0
- package/src/cli/manifest.mjs +7 -0
- package/src/cli/options/certification.mjs +8 -0
- package/src/cli/options/index.mjs +2 -0
- package/src/commands/chat.mjs +1 -0
- package/src/commands/doctor.mjs +55 -1
- package/src/commands/execute.mjs +4 -0
- package/src/commands/wallet.mjs +3 -0
- package/src/runtime/actions/selection.mjs +1 -6
- package/src/runtime/certification/executionCertify.mjs +378 -0
- package/src/runtime/certification/venueActionSchemas.mjs +195 -0
- package/src/runtime/handlers.mjs +8 -0
- package/src/runtime/main.mjs +11 -1
- package/src/runtime/validation/actionParamsGate.mjs +74 -0
- package/src/runtime/validation/paramsSchema.mjs +127 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ All notable changes to `@thirdfy/agent-cli` are documented here. The format is b
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.2.31] - 2026-07-23
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Catalog-driven local `paramsSchema` validation on `preflight`, `run`, `wallet execute`, `wallet sign`, and `self-exec` before any HTTP write. Unresolved executable actions and write actions without catalog `paramsSchema` fail closed with `PARAMS_SCHEMA_INVALID`; read-only catalog actions keep schema-skip behavior.
|
|
12
|
+
- `doctor certify execution` no-write certification report: CLI version, execution command availability, Hyperliquid/Lighter/Polymarket place/cancel/leverage schema matrix, supported authority input sources, and EarnClaw control-plane flags (`--no-write`, `--run-mode`, `--schema-digest`, `--required-actions`). Bare certification requires a live action catalog; embedded fixtures remain diagnostic only.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Client validation strips billing hints (`estimatedAmountUsd`, `amountUsd`) from action params before schema checks (parity with API execute paths).
|
|
17
|
+
- Whitelist `cexCredentialRef` alongside `agentKey` when catalog `additionalProperties` is false (orchestration envelope key, not action params).
|
|
18
|
+
|
|
7
19
|
## [0.2.30] - 2026-07-19
|
|
8
20
|
|
|
9
21
|
### Changed
|
package/README.md
CHANGED
|
@@ -40,10 +40,10 @@ Run without global install:
|
|
|
40
40
|
npx @thirdfy/agent-cli --help
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
## What's new in v0.2.
|
|
43
|
+
## What's new in v0.2.31
|
|
44
44
|
|
|
45
|
-
-
|
|
46
|
-
-
|
|
45
|
+
- Validates action `--params` against catalog `paramsSchema` locally on `preflight`, `run`, and `wallet execute` before HTTP writes. Failures return `PARAMS_SCHEMA_INVALID` with field-level errors.
|
|
46
|
+
- Adds `doctor certify execution` for a no-write certification report (CLI version, command availability, venue schema matrix, authority inputs). EarnClaw passes `--no-write`, `--run-mode`, `--schema-digest`, and `--required-actions` for fail-closed pack certification.
|
|
47
47
|
|
|
48
48
|
Older versions: see [CHANGELOG.md](./CHANGELOG.md) and [GitHub Releases](https://github.com/thirdfy/agent-cli/releases).
|
|
49
49
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thirdfy/agent-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.31",
|
|
4
4
|
"description": "Thirdfy Agent CLI for onboarding, governance preflight, execute-intent, and status polling.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
],
|
|
24
24
|
"scripts": {
|
|
25
25
|
"test": "node --test \"test/**/*.test.cjs\"",
|
|
26
|
-
"lint": "node --check ./bin/thirdfy-agent.mjs && node --check ./src/core/*.mjs && node --check ./src/commands/*.mjs && node --check ./src/cli/*.mjs && node --check ./src/cli/options/*.mjs && node --check ./src/runtime/*.mjs && node --check ./src/runtime/execution/*.mjs && node --check ./src/runtime/actions/*.mjs",
|
|
26
|
+
"lint": "node --check ./bin/thirdfy-agent.mjs && node --check ./src/core/*.mjs && node --check ./src/commands/*.mjs && node --check ./src/cli/*.mjs && node --check ./src/cli/options/*.mjs && node --check ./src/runtime/*.mjs && node --check ./src/runtime/execution/*.mjs && node --check ./src/runtime/actions/*.mjs && node --check ./src/runtime/validation/*.mjs && node --check ./src/runtime/certification/*.mjs",
|
|
27
27
|
"validate:cli-manifest-handlers": "node ./scripts/validate-cli-manifest-handlers.mjs",
|
|
28
28
|
"validate:runtime-boundaries": "node ./scripts/validate-runtime-boundaries.mjs",
|
|
29
29
|
"validate:npm-pack": "node ./scripts/validate-npm-pack.mjs",
|
package/src/cli/help.mjs
CHANGED
|
@@ -62,6 +62,7 @@ Core commands:
|
|
|
62
62
|
thirdfy-agent self-exec --agent-api-key <key> --action <id> --params <json> --wallet-address <addr> --ows-wallet-name <name> [--rpc-url <url>] [--effect-check strict|relaxed|off] [--json]
|
|
63
63
|
thirdfy-agent doctor self [--wallet-address <addr>] [--ows-wallet-name <name>] [--rpc-url <url>] [--json]
|
|
64
64
|
thirdfy-agent doctor auth [--json]
|
|
65
|
+
thirdfy-agent doctor certify execution [--no-write] [--run-mode <mode>] [--schema-digest <digest>] [--required-actions <csv>] [--json]
|
|
65
66
|
thirdfy-agent jeff preflight --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--json]
|
|
66
67
|
thirdfy-agent jeff trade --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--json]
|
|
67
68
|
thirdfy-agent jeff status --intent-id <id> [--json]
|
package/src/cli/manifest.mjs
CHANGED
|
@@ -62,6 +62,13 @@ export const CLI_MANIFEST = [
|
|
|
62
62
|
{ path: ['whoami'], handler: 'commandWhoami', description: 'Show masked local identity' },
|
|
63
63
|
{ path: ['doctor', 'self'], handler: 'commandDoctorSelf', options: ['wallet'], description: 'Self-mode wallet diagnostics' },
|
|
64
64
|
{ path: ['doctor', 'auth'], handler: 'commandDoctorAuth', description: 'Auth and onboarding diagnostics' },
|
|
65
|
+
{
|
|
66
|
+
path: ['doctor', 'certify', 'execution'],
|
|
67
|
+
handler: 'commandDoctorCertifyExecution',
|
|
68
|
+
tier: 'offline',
|
|
69
|
+
options: ['certification'],
|
|
70
|
+
description: 'No-write execution certification (CLI version, commands, venue schema matrix)',
|
|
71
|
+
},
|
|
65
72
|
{
|
|
66
73
|
path: ['help', 'onboarding'],
|
|
67
74
|
handler: 'commandHelpOnboarding',
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Options for `doctor certify execution` (no-write control-plane certification). */
|
|
2
|
+
export function attachCertificationOptions(cmd) {
|
|
3
|
+
cmd
|
|
4
|
+
.option('--no-write', 'assert certification performs no HTTP writes (always true for this command)')
|
|
5
|
+
.option('--schema-digest <digest>', 'expected sha256 digest for required action params schemas')
|
|
6
|
+
.option('--required-actions <csv>', 'comma-separated Thirdfy action ids required by the runtime pack');
|
|
7
|
+
return cmd;
|
|
8
|
+
}
|
|
@@ -15,6 +15,7 @@ import { attachCreditsOptions } from './credits.mjs';
|
|
|
15
15
|
import { attachChatOptions } from './chat.mjs';
|
|
16
16
|
import { attachVenueOptions } from './venue.mjs';
|
|
17
17
|
import { attachBrainOptions } from './brain.mjs';
|
|
18
|
+
import { attachCertificationOptions } from './certification.mjs';
|
|
18
19
|
|
|
19
20
|
const BUNDLES = {
|
|
20
21
|
auth: attachAuthOptions,
|
|
@@ -33,6 +34,7 @@ const BUNDLES = {
|
|
|
33
34
|
chat: attachChatOptions,
|
|
34
35
|
venue: attachVenueOptions,
|
|
35
36
|
brain: attachBrainOptions,
|
|
37
|
+
certification: attachCertificationOptions,
|
|
36
38
|
};
|
|
37
39
|
|
|
38
40
|
/** @param {import('commander').Command} cmd */
|
package/src/commands/chat.mjs
CHANGED
package/src/commands/doctor.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createRequire } from 'module';
|
|
2
2
|
import { requireFlag } from '../core/args.mjs';
|
|
3
3
|
import { apiGet, requestWithRouteFallback } from '../core/http.mjs';
|
|
4
|
-
import { loadProfileConfig, getProfileConfigPath } from '../core/context.mjs';
|
|
4
|
+
import { loadProfileConfig, getProfileConfigPath, getCachedActionsCatalog } from '../core/context.mjs';
|
|
5
5
|
import { DEFAULT_API_BASE, PROFILE_DEFAULTS } from '../core/constants.mjs';
|
|
6
6
|
import { normalizeRunMode, normalizeManagedRunMode } from '../core/runMode.mjs';
|
|
7
|
+
import { buildExecutionCertificationReport } from '../runtime/certification/executionCertify.mjs';
|
|
7
8
|
|
|
8
9
|
const require = createRequire(import.meta.url);
|
|
9
10
|
const { normalizeDelegationStatus } = require('../utils/cli/delegationNormalization.cjs');
|
|
@@ -18,6 +19,7 @@ export function createDoctorCommands({
|
|
|
18
19
|
resolveEffectiveUserDid,
|
|
19
20
|
loadEthers,
|
|
20
21
|
resolveOwsBinary,
|
|
22
|
+
CLI_VERSION,
|
|
21
23
|
}) {
|
|
22
24
|
async function commandDoctorSelf(ctx, flags) {
|
|
23
25
|
const walletAddress = String(flags.walletAddress || process.env.MARKET_MAKER_WALLET_ADDRESS || '').trim();
|
|
@@ -311,10 +313,62 @@ async function commandManagedSetup(ctx, flags, capabilities) {
|
|
|
311
313
|
if (!success) process.exit(1);
|
|
312
314
|
}
|
|
313
315
|
|
|
316
|
+
async function commandDoctorCertifyExecution(ctx, flags) {
|
|
317
|
+
let liveActions = null;
|
|
318
|
+
let catalogError = null;
|
|
319
|
+
try {
|
|
320
|
+
liveActions = await getCachedActionsCatalog(ctx, flags);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
catalogError = {
|
|
323
|
+
message: error?.message || 'Actions catalog fetch failed',
|
|
324
|
+
statusCode: Number(error?.statusCode || 0) || undefined,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const requiredActions = String(flags.requiredActions || '')
|
|
329
|
+
.split(',')
|
|
330
|
+
.map((item) => item.trim())
|
|
331
|
+
.filter(Boolean);
|
|
332
|
+
const runMode = String(flags.runMode || '').trim() || null;
|
|
333
|
+
const expectedSchemaDigest = String(flags.schemaDigest || '').trim() || null;
|
|
334
|
+
|
|
335
|
+
const report = buildExecutionCertificationReport({
|
|
336
|
+
cliVersion: CLI_VERSION,
|
|
337
|
+
liveActions,
|
|
338
|
+
runMode,
|
|
339
|
+
expectedSchemaDigest,
|
|
340
|
+
requiredActions,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
printEnvelope({
|
|
344
|
+
success: report.ok,
|
|
345
|
+
code: report.ok ? 'CERTIFY_EXECUTION_OK' : 'CERTIFY_EXECUTION_FAILED',
|
|
346
|
+
message: report.ok
|
|
347
|
+
? 'Execution certification passed (no HTTP writes performed)'
|
|
348
|
+
: 'Execution certification found blocking issues',
|
|
349
|
+
data: {
|
|
350
|
+
...report,
|
|
351
|
+
catalog: {
|
|
352
|
+
fetched: Array.isArray(liveActions),
|
|
353
|
+
actionCount: Array.isArray(liveActions) ? liveActions.length : 0,
|
|
354
|
+
error: catalogError,
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
meta: {
|
|
358
|
+
apiBase: ctx.apiBase,
|
|
359
|
+
writeFree: true,
|
|
360
|
+
noWrite: true,
|
|
361
|
+
cliVersion: CLI_VERSION,
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
if (!report.ok) process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
|
|
314
367
|
return {
|
|
315
368
|
commandDoctorSelf,
|
|
316
369
|
buildOnboardingHelp,
|
|
317
370
|
commandDoctorAuth,
|
|
318
371
|
commandManagedSetup,
|
|
372
|
+
commandDoctorCertifyExecution,
|
|
319
373
|
};
|
|
320
374
|
}
|
package/src/commands/execute.mjs
CHANGED
|
@@ -16,6 +16,7 @@ export function createExecuteCommands(deps) {
|
|
|
16
16
|
const {
|
|
17
17
|
resolveActionSelection,
|
|
18
18
|
prepareActionParamsForFlags,
|
|
19
|
+
enforceCatalogActionParams,
|
|
19
20
|
enforceOffchainVenueLaneCompatibility,
|
|
20
21
|
enforceChainCompatibility,
|
|
21
22
|
applyHyperliquidAgentWalletAddressDefault,
|
|
@@ -35,6 +36,7 @@ export function createExecuteCommands(deps) {
|
|
|
35
36
|
async function commandPreflight(ctx, flags, capabilities) {
|
|
36
37
|
const resolved = await resolveActionSelection(ctx, flags);
|
|
37
38
|
prepareActionParamsForFlags(flags, resolved.resolvedAction);
|
|
39
|
+
enforceCatalogActionParams(resolved, flags);
|
|
38
40
|
const runMode = getEffectiveRunMode(ctx, flags);
|
|
39
41
|
enforceOffchainVenueLaneCompatibility(resolved.resolvedAction, runMode);
|
|
40
42
|
const hybridWalletMode = runMode === 'hybrid' ? normalizeHybridWalletMode(flags.hybridWalletMode) : null;
|
|
@@ -87,6 +89,7 @@ async function commandPreflight(ctx, flags, capabilities) {
|
|
|
87
89
|
async function commandRun(ctx, flags, capabilities) {
|
|
88
90
|
const resolved = await resolveActionSelection(ctx, flags);
|
|
89
91
|
prepareActionParamsForFlags(flags, resolved.resolvedAction);
|
|
92
|
+
enforceCatalogActionParams(resolved, flags);
|
|
90
93
|
const runMode = getEffectiveRunMode(ctx, flags);
|
|
91
94
|
enforceOffchainVenueLaneCompatibility(resolved.resolvedAction, runMode);
|
|
92
95
|
const hybridWalletMode = runMode === 'hybrid' ? normalizeHybridWalletMode(flags.hybridWalletMode) : null;
|
|
@@ -212,6 +215,7 @@ async function commandIntentStatus(ctx, flags, capabilities) {
|
|
|
212
215
|
async function commandSelfExec(ctx, flags, capabilities) {
|
|
213
216
|
const resolved = await resolveActionSelection(ctx, flags);
|
|
214
217
|
prepareActionParamsForFlags(flags, resolved.resolvedAction);
|
|
218
|
+
enforceCatalogActionParams(resolved, flags);
|
|
215
219
|
enforceOffchainVenueLaneCompatibility(resolved.resolvedAction, 'self');
|
|
216
220
|
enforceChainCompatibility(capabilities, flags, 'self');
|
|
217
221
|
const selfResult = await executeSelfRun(ctx, flags, resolved, {
|
package/src/commands/wallet.mjs
CHANGED
|
@@ -6,6 +6,7 @@ export function createWalletCommands(deps) {
|
|
|
6
6
|
const {
|
|
7
7
|
resolveActionSelection,
|
|
8
8
|
prepareActionParamsForFlags,
|
|
9
|
+
enforceCatalogActionParams,
|
|
9
10
|
enforceOffchainVenueLaneCompatibility,
|
|
10
11
|
enforceChainCompatibility,
|
|
11
12
|
applyHyperliquidAgentWalletAddressDefault,
|
|
@@ -21,6 +22,7 @@ async function commandWalletExecute(ctx, flags, capabilities) {
|
|
|
21
22
|
const normalizedFlags = { ...flags, runMode: 'agent_wallet' };
|
|
22
23
|
const resolved = await resolveActionSelection(ctx, normalizedFlags);
|
|
23
24
|
prepareActionParamsForFlags(normalizedFlags, resolved.resolvedAction);
|
|
25
|
+
enforceCatalogActionParams(resolved, normalizedFlags);
|
|
24
26
|
enforceChainCompatibility(capabilities, normalizedFlags, 'agent_wallet');
|
|
25
27
|
const managedPreflight =
|
|
26
28
|
(await applyHyperliquidAgentWalletAddressDefault(ctx, normalizedFlags, resolved, 'agent_wallet'))
|
|
@@ -51,6 +53,7 @@ async function commandWalletSign(ctx, flags, capabilities) {
|
|
|
51
53
|
const normalizedFlags = { ...flags, runMode: 'self' };
|
|
52
54
|
const resolved = await resolveActionSelection(ctx, normalizedFlags);
|
|
53
55
|
prepareActionParamsForFlags(normalizedFlags, resolved.resolvedAction);
|
|
56
|
+
enforceCatalogActionParams(resolved, normalizedFlags);
|
|
54
57
|
enforceOffchainVenueLaneCompatibility(resolved.resolvedAction, 'self');
|
|
55
58
|
enforceChainCompatibility(capabilities, normalizedFlags, 'self');
|
|
56
59
|
const selfResult = await executeSelfRun(ctx, normalizedFlags, resolved, {
|
|
@@ -354,12 +354,7 @@ async function resolveActionSelection(ctx, flags) {
|
|
|
354
354
|
)}?${formatProviderAmbiguousActionHint(providerHint)}`
|
|
355
355
|
);
|
|
356
356
|
}
|
|
357
|
-
|
|
358
|
-
throw new Error(
|
|
359
|
-
`Unknown --action "${requestedAction}". No compatible action found in current catalog/provider scope.${formatProviderUnknownActionHint(
|
|
360
|
-
providerHint
|
|
361
|
-
)}`
|
|
362
|
-
);
|
|
357
|
+
return { requestedAction, resolvedAction: requestedAction, resolvedActionMeta: null };
|
|
363
358
|
}
|
|
364
359
|
|
|
365
360
|
return {
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { createRequire } from 'module';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { CLI_MANIFEST } from '../../cli/manifest.mjs';
|
|
4
|
+
import { validateParamsSchema, sanitizeParamsForSchemaValidation } from '../validation/paramsSchema.mjs';
|
|
5
|
+
import { VENUE_WRITE_ACTION_FIXTURES } from './venueActionSchemas.mjs';
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
|
|
9
|
+
const { version: PACKAGE_VERSION } = require('../../../package.json');
|
|
10
|
+
|
|
11
|
+
const EXECUTION_COMMAND_PATHS = [
|
|
12
|
+
['preflight'],
|
|
13
|
+
['run'],
|
|
14
|
+
['wallet', 'execute'],
|
|
15
|
+
['agent', 'run'],
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const AUTHORITY_INPUT_CHECKS = [
|
|
19
|
+
{ id: 'agentApiKey', sources: ['flag:agentApiKey', 'env:THIRDFY_AGENT_API_KEY', 'config:agent.apiKey'] },
|
|
20
|
+
{ id: 'userDid', sources: ['flag:userDid', 'env:THIRDFY_USER_DID', 'config:wallets.userDid'] },
|
|
21
|
+
{ id: 'agentKey', sources: ['flag:agentKey', 'env:THIRDFY_AGENT_KEY', 'config:agent.agentKey'] },
|
|
22
|
+
{ id: 'estimatedAmountUsd', sources: ['flag:estimatedAmountUsd'] },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function normalizeActionKey(action) {
|
|
26
|
+
return String(action || '').trim().replace(/_/g, '-').toLowerCase();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isWriteAction(action) {
|
|
30
|
+
const key = normalizeActionKey(action);
|
|
31
|
+
return !key.startsWith('get-') && !key.startsWith('list-');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function manifestHasPath(pathSegments) {
|
|
35
|
+
const key = pathSegments.join(' ');
|
|
36
|
+
return CLI_MANIFEST.some((entry) => entry.path.join(' ') === key);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function executionRelevantSchemaSlice(schema) {
|
|
40
|
+
if (!schema || typeof schema !== 'object') return {};
|
|
41
|
+
const properties = schema.properties;
|
|
42
|
+
const required = schema.required;
|
|
43
|
+
const additionalProperties = schema.additionalProperties;
|
|
44
|
+
const normalizedProperties = {};
|
|
45
|
+
if (properties && typeof properties === 'object' && !Array.isArray(properties)) {
|
|
46
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
47
|
+
if (!value || typeof value !== 'object') {
|
|
48
|
+
normalizedProperties[key] = value;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const row = value;
|
|
52
|
+
normalizedProperties[key] = {
|
|
53
|
+
type: row.type ?? null,
|
|
54
|
+
enum: row.enum ?? null,
|
|
55
|
+
minimum: row.minimum ?? null,
|
|
56
|
+
maximum: row.maximum ?? null,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
required: Array.isArray(required) ? [...required].sort() : [],
|
|
62
|
+
additionalProperties: additionalProperties ?? null,
|
|
63
|
+
properties: normalizedProperties,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function computeActionSchemaDigest(actions) {
|
|
68
|
+
const payload = actions
|
|
69
|
+
.map((row) => ({
|
|
70
|
+
action: normalizeActionKey(row.action),
|
|
71
|
+
requiredParams: [...(row.requiredParams || [])].sort(),
|
|
72
|
+
schema: executionRelevantSchemaSlice(row.paramsSchema),
|
|
73
|
+
}))
|
|
74
|
+
.sort((a, b) => a.action.localeCompare(b.action));
|
|
75
|
+
return createHash('sha256').update(JSON.stringify(payload)).digest('hex');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function mergeCatalogWithFixtures(liveActions) {
|
|
79
|
+
const byAction = new Map();
|
|
80
|
+
for (const fixture of VENUE_WRITE_ACTION_FIXTURES) {
|
|
81
|
+
byAction.set(fixture.action, {
|
|
82
|
+
action: fixture.action,
|
|
83
|
+
aliases: fixture.aliases,
|
|
84
|
+
provider: fixture.provider,
|
|
85
|
+
paramsSchema: fixture.paramsSchema,
|
|
86
|
+
requiredParams: fixture.paramsSchema.required || [],
|
|
87
|
+
source: 'fixture',
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
for (const row of liveActions || []) {
|
|
91
|
+
const action = String(row?.action || '').trim();
|
|
92
|
+
if (!action) continue;
|
|
93
|
+
const existing = byAction.get(action);
|
|
94
|
+
byAction.set(action, {
|
|
95
|
+
action,
|
|
96
|
+
aliases: Array.isArray(row.aliases) ? row.aliases : existing?.aliases || [],
|
|
97
|
+
provider: row.provider || row.providerId || existing?.provider || null,
|
|
98
|
+
paramsSchema: row.paramsSchema || existing?.paramsSchema || null,
|
|
99
|
+
requiredParams: Array.isArray(row.requiredParams) ? row.requiredParams : existing?.requiredParams || [],
|
|
100
|
+
source: row.paramsSchema ? 'catalog' : existing?.source || 'catalog',
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return byAction;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function resolveCatalogRow(catalogByAction, action) {
|
|
107
|
+
const direct = catalogByAction.get(action);
|
|
108
|
+
if (direct) return direct;
|
|
109
|
+
const normalized = normalizeActionKey(action);
|
|
110
|
+
for (const row of catalogByAction.values()) {
|
|
111
|
+
if (normalizeActionKey(row.action) === normalized) return row;
|
|
112
|
+
for (const alias of row.aliases || []) {
|
|
113
|
+
if (normalizeActionKey(alias) === normalized) return row;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function runSchemaCompatibilityChecks(catalogByAction, actionsToCheck, { requireLiveCatalog = false } = {}) {
|
|
120
|
+
const fixturesByAction = new Map(VENUE_WRITE_ACTION_FIXTURES.map((fixture) => [fixture.action, fixture]));
|
|
121
|
+
return actionsToCheck.map((actionName) => {
|
|
122
|
+
const fixture = fixturesByAction.get(actionName) || null;
|
|
123
|
+
const catalogRow = resolveCatalogRow(catalogByAction, actionName);
|
|
124
|
+
const liveSchema = catalogRow?.source === 'catalog' ? catalogRow.paramsSchema : null;
|
|
125
|
+
const schema = requireLiveCatalog ? liveSchema : catalogRow?.paramsSchema || fixture?.paramsSchema || null;
|
|
126
|
+
const checks = [];
|
|
127
|
+
|
|
128
|
+
checks.push({
|
|
129
|
+
name: 'schema-present',
|
|
130
|
+
ok: requireLiveCatalog
|
|
131
|
+
? Boolean(liveSchema && typeof liveSchema === 'object')
|
|
132
|
+
: Boolean(schema && typeof schema === 'object'),
|
|
133
|
+
detail: requireLiveCatalog
|
|
134
|
+
? liveSchema
|
|
135
|
+
? 'live catalog paramsSchema available'
|
|
136
|
+
: 'live catalog paramsSchema unavailable'
|
|
137
|
+
: schema
|
|
138
|
+
? 'paramsSchema available'
|
|
139
|
+
: 'missing paramsSchema',
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (fixture?.validSample && schema) {
|
|
143
|
+
const valid = validateParamsSchema(sanitizeParamsForSchemaValidation(fixture.validSample), schema);
|
|
144
|
+
checks.push({
|
|
145
|
+
name: 'valid-sample',
|
|
146
|
+
ok: valid.valid === true,
|
|
147
|
+
detail: valid.valid ? 'reference sample passes validation' : valid.error || 'sample rejected',
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const missingRequired = validateParamsSchema(sanitizeParamsForSchemaValidation({}), schema);
|
|
151
|
+
checks.push({
|
|
152
|
+
name: 'missing-required-rejected',
|
|
153
|
+
ok: missingRequired.valid === false,
|
|
154
|
+
detail: missingRequired.error || 'empty params unexpectedly accepted',
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const badTypeSample = { ...fixture.validSample };
|
|
158
|
+
const firstRequired = Array.isArray(schema.required) ? schema.required[0] : null;
|
|
159
|
+
const firstRule = firstRequired ? schema.properties?.[firstRequired] : null;
|
|
160
|
+
if (firstRequired && firstRule?.type === 'boolean') {
|
|
161
|
+
badTypeSample[firstRequired] = 'not-a-boolean';
|
|
162
|
+
} else if (firstRequired && (firstRule?.type === 'number' || firstRule?.type === 'integer')) {
|
|
163
|
+
badTypeSample[firstRequired] = 'not-a-number';
|
|
164
|
+
} else if (firstRequired) {
|
|
165
|
+
badTypeSample[firstRequired] = 12345;
|
|
166
|
+
}
|
|
167
|
+
const badType = validateParamsSchema(sanitizeParamsForSchemaValidation(badTypeSample), schema);
|
|
168
|
+
checks.push({
|
|
169
|
+
name: 'wrong-type-rejected',
|
|
170
|
+
ok: badType.valid === false,
|
|
171
|
+
detail: badType.error || 'wrong type unexpectedly accepted',
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const unknownMeta = validateParamsSchema(
|
|
175
|
+
sanitizeParamsForSchemaValidation({
|
|
176
|
+
...fixture.validSample,
|
|
177
|
+
totallyUnknownMetadataField: 'adapter',
|
|
178
|
+
}),
|
|
179
|
+
schema
|
|
180
|
+
);
|
|
181
|
+
checks.push({
|
|
182
|
+
name: 'unknown-metadata-rejected',
|
|
183
|
+
ok: unknownMeta.valid === false,
|
|
184
|
+
detail: unknownMeta.error || 'unknown metadata unexpectedly accepted',
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const aliasChecks = (catalogRow?.aliases || fixture?.aliases || []).map((alias) => ({
|
|
189
|
+
alias,
|
|
190
|
+
resolvesTo: catalogRow?.action || fixture?.action || actionName,
|
|
191
|
+
ok: true,
|
|
192
|
+
}));
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
action: catalogRow?.action || fixture?.action || actionName,
|
|
196
|
+
provider: catalogRow?.provider || fixture?.provider || null,
|
|
197
|
+
catalogSource: catalogRow?.source || 'missing',
|
|
198
|
+
authorityInputs: fixture?.authorityInputs || [],
|
|
199
|
+
aliases: aliasChecks,
|
|
200
|
+
checks,
|
|
201
|
+
ok: checks.every((entry) => entry.ok),
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function parseRequiredActions(value) {
|
|
207
|
+
if (!value) return [];
|
|
208
|
+
if (Array.isArray(value)) {
|
|
209
|
+
return value.map((item) => String(item || '').trim()).filter(Boolean);
|
|
210
|
+
}
|
|
211
|
+
return String(value)
|
|
212
|
+
.split(',')
|
|
213
|
+
.map((item) => item.trim())
|
|
214
|
+
.filter(Boolean);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function buildExecutionCertificationReport({
|
|
218
|
+
cliVersion,
|
|
219
|
+
liveActions = null,
|
|
220
|
+
runMode = null,
|
|
221
|
+
expectedSchemaDigest = null,
|
|
222
|
+
requiredActions = [],
|
|
223
|
+
} = {}) {
|
|
224
|
+
const version = String(cliVersion || PACKAGE_VERSION).trim();
|
|
225
|
+
const normalizedRequiredActions = parseRequiredActions(requiredActions);
|
|
226
|
+
const blockers = [];
|
|
227
|
+
|
|
228
|
+
const commandAvailability = EXECUTION_COMMAND_PATHS.map((pathSegments) => ({
|
|
229
|
+
command: pathSegments.join(' '),
|
|
230
|
+
available: manifestHasPath(pathSegments),
|
|
231
|
+
}));
|
|
232
|
+
|
|
233
|
+
const catalogByAction = mergeCatalogWithFixtures(liveActions);
|
|
234
|
+
const catalogFetched = Array.isArray(liveActions);
|
|
235
|
+
const bareCertification = normalizedRequiredActions.length === 0;
|
|
236
|
+
|
|
237
|
+
if (bareCertification && !catalogFetched) {
|
|
238
|
+
blockers.push('LIVE_CATALOG_UNAVAILABLE');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
for (const action of normalizedRequiredActions) {
|
|
242
|
+
const row = resolveCatalogRow(catalogByAction, action);
|
|
243
|
+
if (!row) {
|
|
244
|
+
blockers.push(`REQUIRED_ACTION_MISSING:${action}`);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (isWriteAction(action) && row.source !== 'catalog') {
|
|
248
|
+
blockers.push(`CATALOG_SCHEMA_UNAVAILABLE:${action}`);
|
|
249
|
+
}
|
|
250
|
+
if (isWriteAction(action) && !row.paramsSchema) {
|
|
251
|
+
blockers.push(`CATALOG_SCHEMA_UNAVAILABLE:${action}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const venueActionNames =
|
|
256
|
+
normalizedRequiredActions.length > 0
|
|
257
|
+
? normalizedRequiredActions.filter((action) => isWriteAction(action))
|
|
258
|
+
: VENUE_WRITE_ACTION_FIXTURES.map((fixture) => fixture.action);
|
|
259
|
+
|
|
260
|
+
const venueActions = runSchemaCompatibilityChecks(catalogByAction, venueActionNames, {
|
|
261
|
+
requireLiveCatalog: bareCertification,
|
|
262
|
+
});
|
|
263
|
+
for (const row of venueActions) {
|
|
264
|
+
if (!row.ok) {
|
|
265
|
+
blockers.push(`VENUE_SCHEMA_CHECK_FAILED:${row.action}`);
|
|
266
|
+
}
|
|
267
|
+
if (bareCertification && catalogFetched && row.catalogSource !== 'catalog') {
|
|
268
|
+
blockers.push(`CATALOG_SCHEMA_UNAVAILABLE:${row.action}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const digestInputs = normalizedRequiredActions.map((action) => {
|
|
273
|
+
const row = resolveCatalogRow(catalogByAction, action);
|
|
274
|
+
return {
|
|
275
|
+
action,
|
|
276
|
+
requiredParams: row?.requiredParams || [],
|
|
277
|
+
paramsSchema: row?.paramsSchema || null,
|
|
278
|
+
};
|
|
279
|
+
});
|
|
280
|
+
const actionSchemaDigest =
|
|
281
|
+
digestInputs.length > 0
|
|
282
|
+
? computeActionSchemaDigest(digestInputs)
|
|
283
|
+
: computeActionSchemaDigest(
|
|
284
|
+
VENUE_WRITE_ACTION_FIXTURES.map((fixture) => ({
|
|
285
|
+
action: fixture.action,
|
|
286
|
+
requiredParams: fixture.paramsSchema.required || [],
|
|
287
|
+
paramsSchema: fixture.paramsSchema,
|
|
288
|
+
}))
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
const schemaCompatibility = {
|
|
292
|
+
ok: !expectedSchemaDigest || actionSchemaDigest === String(expectedSchemaDigest).trim(),
|
|
293
|
+
digest: actionSchemaDigest,
|
|
294
|
+
expectedDigest: expectedSchemaDigest ? String(expectedSchemaDigest).trim() : null,
|
|
295
|
+
matchesExpected: !expectedSchemaDigest || actionSchemaDigest === String(expectedSchemaDigest).trim(),
|
|
296
|
+
};
|
|
297
|
+
if (expectedSchemaDigest && !schemaCompatibility.ok) {
|
|
298
|
+
blockers.push('SCHEMA_DIGEST_MISMATCH');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const authorityInputs = AUTHORITY_INPUT_CHECKS.map((entry) => ({
|
|
302
|
+
id: entry.id,
|
|
303
|
+
supported: true,
|
|
304
|
+
sources: entry.sources,
|
|
305
|
+
}));
|
|
306
|
+
|
|
307
|
+
const authority = {
|
|
308
|
+
ok: Boolean(runMode),
|
|
309
|
+
mode: runMode || null,
|
|
310
|
+
};
|
|
311
|
+
if (!authority.ok && normalizedRequiredActions.length > 0) {
|
|
312
|
+
blockers.push('CERTIFICATION_RUN_MODE_MISSING');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const writePolicy = {
|
|
316
|
+
ok: normalizedRequiredActions.every((action) => {
|
|
317
|
+
if (!isWriteAction(action)) return true;
|
|
318
|
+
const row = resolveCatalogRow(catalogByAction, action);
|
|
319
|
+
return row?.source === 'catalog' && Boolean(row?.paramsSchema);
|
|
320
|
+
}),
|
|
321
|
+
};
|
|
322
|
+
if (!writePolicy.ok) {
|
|
323
|
+
blockers.push('CLI_CERTIFICATION_WRITE_POLICY_MISMATCH');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const readiness = {
|
|
327
|
+
ok: blockers.length === 0,
|
|
328
|
+
blockers: [...blockers],
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const checks = [
|
|
332
|
+
{
|
|
333
|
+
name: 'cli-version',
|
|
334
|
+
ok: Boolean(version),
|
|
335
|
+
detail: version || 'missing package version',
|
|
336
|
+
},
|
|
337
|
+
...commandAvailability.map((entry) => ({
|
|
338
|
+
name: `command:${entry.command}`,
|
|
339
|
+
ok: entry.available,
|
|
340
|
+
detail: entry.available ? 'manifest handler registered' : 'command missing from manifest',
|
|
341
|
+
})),
|
|
342
|
+
{
|
|
343
|
+
name: 'venue-action-matrix',
|
|
344
|
+
ok: venueActions.every((entry) => entry.ok),
|
|
345
|
+
detail: `${venueActions.filter((entry) => entry.ok).length}/${venueActions.length} venue write actions pass local schema checks`,
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
name: 'catalog-live-for-certification',
|
|
349
|
+
ok: catalogFetched && blockers.every((item) => !item.startsWith('LIVE_CATALOG_UNAVAILABLE')),
|
|
350
|
+
detail: catalogFetched
|
|
351
|
+
? 'live catalog fetched'
|
|
352
|
+
: bareCertification
|
|
353
|
+
? 'live catalog required for bare certification'
|
|
354
|
+
: 'fixture-only catalog blocked for required write actions',
|
|
355
|
+
},
|
|
356
|
+
];
|
|
357
|
+
|
|
358
|
+
const ok = checks.every((entry) => entry.ok) && blockers.length === 0;
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
ok,
|
|
362
|
+
cliVersion: version,
|
|
363
|
+
certification: 'execution',
|
|
364
|
+
writeFree: true,
|
|
365
|
+
runMode: runMode || null,
|
|
366
|
+
requiredActions: normalizedRequiredActions,
|
|
367
|
+
actionSchemaDigest,
|
|
368
|
+
schemaCompatibility,
|
|
369
|
+
authority,
|
|
370
|
+
writePolicy,
|
|
371
|
+
readiness,
|
|
372
|
+
blockers,
|
|
373
|
+
commandAvailability,
|
|
374
|
+
authorityInputs,
|
|
375
|
+
venueActions,
|
|
376
|
+
checks,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reference paramsSchema fixtures for venue write actions (catalog shape).
|
|
3
|
+
* Used by doctor certify execution and unit tests when live catalog is unavailable.
|
|
4
|
+
*/
|
|
5
|
+
export const VENUE_WRITE_ACTION_FIXTURES = [
|
|
6
|
+
{
|
|
7
|
+
action: 'place_hyperliquid_perps_order',
|
|
8
|
+
aliases: ['place-perps-order', 'place-hyperliquid-perps-order'],
|
|
9
|
+
provider: 'hyperliquid',
|
|
10
|
+
authorityInputs: ['agentApiKey', 'estimatedAmountUsd'],
|
|
11
|
+
paramsSchema: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
additionalProperties: false,
|
|
14
|
+
required: ['coin', 'isBuy', 'size', 'limitPx', 'orderType'],
|
|
15
|
+
properties: {
|
|
16
|
+
coin: { type: 'string', minLength: 1 },
|
|
17
|
+
isBuy: { type: 'boolean' },
|
|
18
|
+
size: { type: 'number', minimum: 0 },
|
|
19
|
+
limitPx: { type: 'number', minimum: 0 },
|
|
20
|
+
orderType: { type: 'string', enum: ['limit', 'market'] },
|
|
21
|
+
reduceOnly: { type: 'boolean' },
|
|
22
|
+
userAddress: { type: 'string' },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
validSample: {
|
|
26
|
+
coin: 'ETH',
|
|
27
|
+
isBuy: true,
|
|
28
|
+
size: 0.01,
|
|
29
|
+
limitPx: 2500,
|
|
30
|
+
orderType: 'limit',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
action: 'cancel_hyperliquid_perps_order',
|
|
35
|
+
aliases: ['cancel-perps-order', 'cancel-hyperliquid-perps-order'],
|
|
36
|
+
provider: 'hyperliquid',
|
|
37
|
+
authorityInputs: ['agentApiKey'],
|
|
38
|
+
paramsSchema: {
|
|
39
|
+
type: 'object',
|
|
40
|
+
additionalProperties: false,
|
|
41
|
+
required: ['coin'],
|
|
42
|
+
properties: {
|
|
43
|
+
coin: { type: 'string', minLength: 1 },
|
|
44
|
+
orderId: { type: 'integer', minimum: 0 },
|
|
45
|
+
cloid: { type: 'string' },
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
validSample: { coin: 'ETH', orderId: 1 },
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
action: 'update_hyperliquid_leverage',
|
|
52
|
+
aliases: ['update-hyperliquid-leverage'],
|
|
53
|
+
provider: 'hyperliquid',
|
|
54
|
+
authorityInputs: ['agentApiKey'],
|
|
55
|
+
paramsSchema: {
|
|
56
|
+
type: 'object',
|
|
57
|
+
additionalProperties: false,
|
|
58
|
+
required: ['coin', 'leverage'],
|
|
59
|
+
properties: {
|
|
60
|
+
coin: { type: 'string', minLength: 1 },
|
|
61
|
+
leverage: { type: 'integer', minimum: 1 },
|
|
62
|
+
isCross: { type: 'boolean' },
|
|
63
|
+
userAddress: { type: 'string' },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
validSample: { coin: 'ETH', leverage: 5 },
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
action: 'place_lighter_perps_order',
|
|
70
|
+
aliases: ['place-lighter-perps-order'],
|
|
71
|
+
provider: 'lighter',
|
|
72
|
+
authorityInputs: ['agentApiKey', 'estimatedAmountUsd'],
|
|
73
|
+
paramsSchema: {
|
|
74
|
+
type: 'object',
|
|
75
|
+
additionalProperties: false,
|
|
76
|
+
required: ['accountIndex', 'apiKeyIndex', 'marketId', 'baseAmount', 'price', 'isAsk'],
|
|
77
|
+
properties: {
|
|
78
|
+
accountIndex: { type: 'integer', minimum: 0 },
|
|
79
|
+
apiKeyIndex: { type: 'integer', minimum: 0 },
|
|
80
|
+
marketId: { type: 'integer', minimum: 0 },
|
|
81
|
+
clientOrderIndex: { type: 'integer', minimum: 0 },
|
|
82
|
+
baseAmount: { type: 'integer', minimum: 1 },
|
|
83
|
+
price: { type: 'integer', minimum: 1 },
|
|
84
|
+
isAsk: { type: 'boolean' },
|
|
85
|
+
orderType: { type: 'string', enum: ['limit', 'market'] },
|
|
86
|
+
reduceOnly: { type: 'boolean' },
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
validSample: {
|
|
90
|
+
accountIndex: 0,
|
|
91
|
+
apiKeyIndex: 0,
|
|
92
|
+
marketId: 1,
|
|
93
|
+
baseAmount: 100,
|
|
94
|
+
price: 250000,
|
|
95
|
+
isAsk: false,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
action: 'cancel_lighter_perps_order',
|
|
100
|
+
aliases: ['cancel-lighter-perps-order'],
|
|
101
|
+
provider: 'lighter',
|
|
102
|
+
authorityInputs: ['agentApiKey'],
|
|
103
|
+
paramsSchema: {
|
|
104
|
+
type: 'object',
|
|
105
|
+
additionalProperties: false,
|
|
106
|
+
required: ['accountIndex', 'apiKeyIndex', 'marketId'],
|
|
107
|
+
properties: {
|
|
108
|
+
accountIndex: { type: 'integer', minimum: 0 },
|
|
109
|
+
apiKeyIndex: { type: 'integer', minimum: 0 },
|
|
110
|
+
marketId: { type: 'integer', minimum: 0 },
|
|
111
|
+
orderIndex: { type: 'integer', minimum: 0 },
|
|
112
|
+
clientOrderIndex: { type: 'integer', minimum: 0 },
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
validSample: {
|
|
116
|
+
accountIndex: 0,
|
|
117
|
+
apiKeyIndex: 0,
|
|
118
|
+
marketId: 1,
|
|
119
|
+
orderIndex: 42,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
action: 'update_lighter_leverage',
|
|
124
|
+
aliases: ['update-lighter-leverage'],
|
|
125
|
+
provider: 'lighter',
|
|
126
|
+
authorityInputs: ['agentApiKey'],
|
|
127
|
+
paramsSchema: {
|
|
128
|
+
type: 'object',
|
|
129
|
+
additionalProperties: false,
|
|
130
|
+
required: ['accountIndex', 'apiKeyIndex', 'marketId', 'leverage'],
|
|
131
|
+
properties: {
|
|
132
|
+
accountIndex: { type: 'integer', minimum: 0 },
|
|
133
|
+
apiKeyIndex: { type: 'integer', minimum: 0 },
|
|
134
|
+
marketId: { type: 'integer', minimum: 0 },
|
|
135
|
+
leverage: { type: 'integer', minimum: 1 },
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
validSample: {
|
|
139
|
+
accountIndex: 0,
|
|
140
|
+
apiKeyIndex: 0,
|
|
141
|
+
marketId: 1,
|
|
142
|
+
leverage: 3,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
action: 'place_polymarket_order',
|
|
147
|
+
aliases: ['place-prediction-order', 'place-polymarket-order'],
|
|
148
|
+
provider: 'polymarket',
|
|
149
|
+
authorityInputs: ['agentApiKey', 'estimatedAmountUsd'],
|
|
150
|
+
paramsSchema: {
|
|
151
|
+
type: 'object',
|
|
152
|
+
additionalProperties: false,
|
|
153
|
+
required: ['tokenID', 'price', 'size', 'side'],
|
|
154
|
+
properties: {
|
|
155
|
+
tokenID: { type: 'string', minLength: 1 },
|
|
156
|
+
price: { type: 'number', minimum: 0, maximum: 1 },
|
|
157
|
+
size: { type: 'number', minimum: 0 },
|
|
158
|
+
side: { type: 'string', enum: ['BUY', 'SELL'] },
|
|
159
|
+
orderType: { type: 'string', enum: ['GTC', 'GTD'] },
|
|
160
|
+
idempotencyKey: { type: 'string', minLength: 1 },
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
validSample: {
|
|
164
|
+
tokenID: '12345',
|
|
165
|
+
price: 0.42,
|
|
166
|
+
size: 10,
|
|
167
|
+
side: 'BUY',
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
action: 'cancel_polymarket_order',
|
|
172
|
+
aliases: ['cancel-prediction-order', 'cancel-polymarket-order'],
|
|
173
|
+
provider: 'polymarket',
|
|
174
|
+
authorityInputs: ['agentApiKey'],
|
|
175
|
+
paramsSchema: {
|
|
176
|
+
type: 'object',
|
|
177
|
+
additionalProperties: false,
|
|
178
|
+
required: ['orderId'],
|
|
179
|
+
properties: {
|
|
180
|
+
orderId: { type: 'string', minLength: 1 },
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
validSample: { orderId: '0xabc' },
|
|
184
|
+
},
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
export function fixtureCatalogActions() {
|
|
188
|
+
return VENUE_WRITE_ACTION_FIXTURES.map((fixture) => ({
|
|
189
|
+
action: fixture.action,
|
|
190
|
+
aliases: fixture.aliases,
|
|
191
|
+
provider: fixture.provider,
|
|
192
|
+
paramsSchema: fixture.paramsSchema,
|
|
193
|
+
requiredParams: fixture.paramsSchema.required || [],
|
|
194
|
+
}));
|
|
195
|
+
}
|
package/src/runtime/handlers.mjs
CHANGED
|
@@ -39,6 +39,7 @@ export function createRuntimeHandlers(deps) {
|
|
|
39
39
|
getPreparedParams,
|
|
40
40
|
resolveActionSelection,
|
|
41
41
|
prepareActionParamsForFlags,
|
|
42
|
+
enforceCatalogActionParams,
|
|
42
43
|
enforceOffchainVenueLaneCompatibility,
|
|
43
44
|
enforceChainCompatibility,
|
|
44
45
|
executeManagedWalletRun,
|
|
@@ -50,6 +51,9 @@ export function createRuntimeHandlers(deps) {
|
|
|
50
51
|
performSelfBroadcast,
|
|
51
52
|
resolveManagedExecutionPreflight,
|
|
52
53
|
resolveEffectiveUserDid,
|
|
54
|
+
loadEthers,
|
|
55
|
+
resolveOwsBinary,
|
|
56
|
+
CLI_VERSION,
|
|
53
57
|
} = deps;
|
|
54
58
|
|
|
55
59
|
const __discovery = createDiscoveryCommands({
|
|
@@ -103,6 +107,7 @@ export function createRuntimeHandlers(deps) {
|
|
|
103
107
|
resolveEffectiveUserDid,
|
|
104
108
|
loadEthers: deps.loadEthers,
|
|
105
109
|
resolveOwsBinary: deps.resolveOwsBinary,
|
|
110
|
+
CLI_VERSION,
|
|
106
111
|
});
|
|
107
112
|
|
|
108
113
|
const __prompt = createPromptCommands({ printEnvelope });
|
|
@@ -123,6 +128,7 @@ export function createRuntimeHandlers(deps) {
|
|
|
123
128
|
const __wallet = createWalletCommands({
|
|
124
129
|
resolveActionSelection,
|
|
125
130
|
prepareActionParamsForFlags,
|
|
131
|
+
enforceCatalogActionParams,
|
|
126
132
|
enforceOffchainVenueLaneCompatibility,
|
|
127
133
|
enforceChainCompatibility,
|
|
128
134
|
applyHyperliquidAgentWalletAddressDefault,
|
|
@@ -136,6 +142,7 @@ export function createRuntimeHandlers(deps) {
|
|
|
136
142
|
const __execute = createExecuteCommands({
|
|
137
143
|
resolveActionSelection,
|
|
138
144
|
prepareActionParamsForFlags,
|
|
145
|
+
enforceCatalogActionParams,
|
|
139
146
|
enforceOffchainVenueLaneCompatibility,
|
|
140
147
|
enforceChainCompatibility,
|
|
141
148
|
applyHyperliquidAgentWalletAddressDefault,
|
|
@@ -203,6 +210,7 @@ export function createRuntimeHandlers(deps) {
|
|
|
203
210
|
...__venue,
|
|
204
211
|
commandDoctorSelf: __doctor.commandDoctorSelf,
|
|
205
212
|
commandDoctorAuth: __doctor.commandDoctorAuth,
|
|
213
|
+
commandDoctorCertifyExecution: __doctor.commandDoctorCertifyExecution,
|
|
206
214
|
commandManagedSetup: __doctor.commandManagedSetup,
|
|
207
215
|
commandCatalogsList: __discovery.commandCatalogsList,
|
|
208
216
|
commandActions: __discovery.commandActions,
|
package/src/runtime/main.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import { createExecutionRunners } from './execution/runners.mjs';
|
|
|
21
21
|
import { createActionSelection } from './actions/selection.mjs';
|
|
22
22
|
import { createBitfinexProbe } from './bitfinexProbe.mjs';
|
|
23
23
|
import { createOwsSigning } from './owsSigning.mjs';
|
|
24
|
+
import { enforceCatalogActionParams } from './validation/actionParamsGate.mjs';
|
|
24
25
|
|
|
25
26
|
const require = createRequire(import.meta.url);
|
|
26
27
|
const { version: CLI_VERSION } = require('../../package.json');
|
|
@@ -70,6 +71,8 @@ export function createThirdfyAgentRuntime() {
|
|
|
70
71
|
formatProviderAmbiguousActionHint,
|
|
71
72
|
formatProviderUnknownActionHint,
|
|
72
73
|
});
|
|
74
|
+
const catalogActionParamsGate = (resolved, flags) =>
|
|
75
|
+
enforceCatalogActionParams(resolved, flags, amounts.getPreparedParams);
|
|
73
76
|
const ows = createOwsSigning();
|
|
74
77
|
const bitfinex = createBitfinexProbe();
|
|
75
78
|
const runners = createExecutionRunners({
|
|
@@ -107,6 +110,7 @@ export function createThirdfyAgentRuntime() {
|
|
|
107
110
|
getPreparedParams: amounts.getPreparedParams,
|
|
108
111
|
resolveActionSelection: actions.resolveActionSelection,
|
|
109
112
|
prepareActionParamsForFlags: amounts.prepareActionParamsForFlags,
|
|
113
|
+
enforceCatalogActionParams: catalogActionParamsGate,
|
|
110
114
|
enforceOffchainVenueLaneCompatibility: lanes.enforceOffchainVenueLaneCompatibility,
|
|
111
115
|
enforceChainCompatibility: lanes.enforceChainCompatibility,
|
|
112
116
|
executeManagedWalletRun: runners.executeManagedWalletRun,
|
|
@@ -120,6 +124,7 @@ export function createThirdfyAgentRuntime() {
|
|
|
120
124
|
resolveEffectiveUserDid: payloads.resolveEffectiveUserDid,
|
|
121
125
|
loadEthers: ows.loadEthers,
|
|
122
126
|
resolveOwsBinary: ows.resolveOwsBinary,
|
|
127
|
+
CLI_VERSION,
|
|
123
128
|
});
|
|
124
129
|
|
|
125
130
|
cliProgram = buildCliProgram({
|
|
@@ -178,15 +183,20 @@ export function createThirdfyAgentRuntime() {
|
|
|
178
183
|
const payload = error?.payload && typeof error.payload === 'object' ? error.payload : {};
|
|
179
184
|
const blockedReason = String(payload.blockedReason || payload.error || '').trim() || undefined;
|
|
180
185
|
const blockedStage = String(payload.blockedStage || '').trim() || undefined;
|
|
186
|
+
const clientCode = String(payload.error || payload.blockedReason || '').trim() || undefined;
|
|
181
187
|
const onboardingHint = onboarding.buildTopLevelOnboardingHint(payload);
|
|
182
188
|
printEnvelope({
|
|
183
189
|
success: false,
|
|
184
|
-
code: statusCode ? 'API_REQUEST_FAILED' : 'CLI_UNHANDLED_ERROR',
|
|
190
|
+
code: statusCode ? 'API_REQUEST_FAILED' : clientCode || 'CLI_UNHANDLED_ERROR',
|
|
185
191
|
message: error?.message || 'Unhandled CLI error',
|
|
186
192
|
data: {
|
|
187
193
|
statusCode,
|
|
188
194
|
blockedReason,
|
|
189
195
|
blockedStage,
|
|
196
|
+
validationError: payload.validationError || null,
|
|
197
|
+
field: payload.field || null,
|
|
198
|
+
resolvedAction: payload.resolvedAction || null,
|
|
199
|
+
requestedAction: payload.requestedAction || null,
|
|
190
200
|
onboardingHint,
|
|
191
201
|
payload,
|
|
192
202
|
},
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { createCliError } from '../../core/envelope.mjs';
|
|
2
|
+
import {
|
|
3
|
+
sanitizeParamsForSchemaValidation,
|
|
4
|
+
validateParamsSchema,
|
|
5
|
+
formatParamsSchemaValidationResult,
|
|
6
|
+
} from './paramsSchema.mjs';
|
|
7
|
+
|
|
8
|
+
export function resolveActionParamsSchema(resolved) {
|
|
9
|
+
const meta = resolved?.resolvedActionMeta;
|
|
10
|
+
if (!meta || typeof meta !== 'object') return null;
|
|
11
|
+
return meta.paramsSchema && typeof meta.paramsSchema === 'object' ? meta.paramsSchema : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isReadOnlyCatalogAction(resolved) {
|
|
15
|
+
const meta = resolved?.resolvedActionMeta;
|
|
16
|
+
if (!meta || typeof meta !== 'object') return false;
|
|
17
|
+
const action = String(resolved?.resolvedAction || '')
|
|
18
|
+
.trim()
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.replace(/-/g, '_');
|
|
21
|
+
const readOnlyByName = action.startsWith('get_') || action.startsWith('list_') || action.startsWith('search_');
|
|
22
|
+
const readOnlyByMeta = meta.supportsExecute === false && meta.supportsExecuteIntent !== false;
|
|
23
|
+
return readOnlyByName || readOnlyByMeta;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function validateResolvedActionParams(resolved, params) {
|
|
27
|
+
const meta = resolved?.resolvedActionMeta;
|
|
28
|
+
if (!meta || typeof meta !== 'object') {
|
|
29
|
+
return {
|
|
30
|
+
valid: false,
|
|
31
|
+
skipped: false,
|
|
32
|
+
error: `Action "${resolved?.requestedAction || ''}" was not found in the Thirdfy catalog`,
|
|
33
|
+
field: null,
|
|
34
|
+
code: 'PARAMS_SCHEMA_INVALID',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const schema = resolveActionParamsSchema(resolved);
|
|
39
|
+
if (!schema) {
|
|
40
|
+
if (isReadOnlyCatalogAction(resolved)) {
|
|
41
|
+
return { valid: true, skipped: true };
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
valid: false,
|
|
45
|
+
skipped: false,
|
|
46
|
+
error: `Action "${resolved.resolvedAction}" has no paramsSchema in the Thirdfy catalog`,
|
|
47
|
+
field: null,
|
|
48
|
+
code: 'PARAMS_SCHEMA_INVALID',
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const sanitized = sanitizeParamsForSchemaValidation(params);
|
|
53
|
+
const result = formatParamsSchemaValidationResult(validateParamsSchema(sanitized, schema));
|
|
54
|
+
return { ...result, skipped: false, sanitizedParams: sanitized };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function enforceCatalogActionParams(resolved, flags, getPreparedParams) {
|
|
58
|
+
const params = typeof getPreparedParams === 'function' ? getPreparedParams(flags) : flags?.__preparedParams || {};
|
|
59
|
+
const result = validateResolvedActionParams(resolved, params);
|
|
60
|
+
if (result.valid) {
|
|
61
|
+
if (result.sanitizedParams && flags) {
|
|
62
|
+
flags.__preparedParams = result.sanitizedParams;
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
throw createCliError('PARAMS_SCHEMA_INVALID', result.error || 'Action params failed catalog schema validation', {
|
|
67
|
+
blockedReason: 'PARAMS_SCHEMA_INVALID',
|
|
68
|
+
blockedStage: 'client_validation',
|
|
69
|
+
validationError: result.error || null,
|
|
70
|
+
field: result.field || null,
|
|
71
|
+
resolvedAction: resolved?.resolvedAction || null,
|
|
72
|
+
requestedAction: resolved?.requestedAction || null,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/** Adapter-only execute metadata tolerated when additionalProperties is false (API parity). */
|
|
2
|
+
export const PARAMS_SCHEMA_ADAPTER_METADATA_KEYS = new Set([
|
|
3
|
+
'resolvedSizingSource',
|
|
4
|
+
'requestedNotionalUsd',
|
|
5
|
+
'clampedToRiskNotionalUsd',
|
|
6
|
+
'clampedToPositionNotionalUsd',
|
|
7
|
+
'adapterRiskNotionalUsd',
|
|
8
|
+
'adapterStopDistancePct',
|
|
9
|
+
'adapterStopDistancePctSource',
|
|
10
|
+
'adapterRiskBudgetUsd',
|
|
11
|
+
'notionalUsd',
|
|
12
|
+
'marginUsd',
|
|
13
|
+
'effectiveLeverage',
|
|
14
|
+
'effectiveNotionalUsd',
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
/** Billing / sizing hints belong on the HTTP payload, not inside action params. */
|
|
18
|
+
export const PARAMS_SCHEMA_BILLING_KEYS = new Set([
|
|
19
|
+
'estimatedAmountUsd',
|
|
20
|
+
'amountUsd',
|
|
21
|
+
'estimated_amount_usd',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
function checkType(value, expected) {
|
|
25
|
+
if (expected === 'string') return typeof value === 'string';
|
|
26
|
+
if (expected === 'number') return typeof value === 'number' && Number.isFinite(value);
|
|
27
|
+
if (expected === 'integer') return typeof value === 'number' && Number.isInteger(value);
|
|
28
|
+
if (expected === 'boolean') return typeof value === 'boolean';
|
|
29
|
+
if (expected === 'object') return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
30
|
+
if (expected === 'array') return Array.isArray(value);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function extractFieldFromError(message) {
|
|
35
|
+
const text = String(message || '');
|
|
36
|
+
let match = text.match(/missing required key "([^"]+)"/);
|
|
37
|
+
if (match) return match[1];
|
|
38
|
+
match = text.match(/unknown key "([^"]+)"/);
|
|
39
|
+
if (match) return match[1];
|
|
40
|
+
match = text.match(/"([^"]+)" must be/);
|
|
41
|
+
if (match) return match[1];
|
|
42
|
+
match = text.match(/"([^"]+)" is too/);
|
|
43
|
+
if (match) return match[1];
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Validate action params against catalog paramsSchema (JSON Schema subset used by Thirdfy API).
|
|
49
|
+
* Mirrors AgentExecutionService.validateParamsSchema for client-side fail-fast.
|
|
50
|
+
*/
|
|
51
|
+
export function validateParamsSchema(params, schema) {
|
|
52
|
+
if (!schema || typeof schema !== 'object') return { valid: true };
|
|
53
|
+
|
|
54
|
+
const required = Array.isArray(schema.required) ? schema.required.map((k) => String(k)) : [];
|
|
55
|
+
const properties =
|
|
56
|
+
schema.properties && typeof schema.properties === 'object'
|
|
57
|
+
? schema.properties
|
|
58
|
+
: {};
|
|
59
|
+
const additionalProperties = schema.additionalProperties;
|
|
60
|
+
|
|
61
|
+
for (const key of required) {
|
|
62
|
+
if (!(key in (params || {}))) {
|
|
63
|
+
const error = `Invalid paramsSchema: missing required key "${key}"`;
|
|
64
|
+
return { valid: false, error, field: key, code: 'PARAMS_SCHEMA_INVALID' };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const [key, value] of Object.entries(params || {})) {
|
|
69
|
+
const rule = properties[key];
|
|
70
|
+
if (!rule) {
|
|
71
|
+
if (additionalProperties === false) {
|
|
72
|
+
if (key === 'agentKey' || key === 'cexCredentialRef' || PARAMS_SCHEMA_ADAPTER_METADATA_KEYS.has(key)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const error = `Invalid paramsSchema: unknown key "${key}"`;
|
|
76
|
+
return { valid: false, error, field: key, code: 'PARAMS_SCHEMA_INVALID' };
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (rule.type && !checkType(value, String(rule.type))) {
|
|
81
|
+
const error = `Invalid paramsSchema: "${key}" must be ${String(rule.type)}`;
|
|
82
|
+
return { valid: false, error, field: key, code: 'PARAMS_SCHEMA_INVALID' };
|
|
83
|
+
}
|
|
84
|
+
if (Array.isArray(rule.enum) && !rule.enum.includes(value)) {
|
|
85
|
+
const error = `Invalid paramsSchema: "${key}" must be one of [${rule.enum.join(', ')}]`;
|
|
86
|
+
return { valid: false, error, field: key, code: 'PARAMS_SCHEMA_INVALID' };
|
|
87
|
+
}
|
|
88
|
+
if (typeof value === 'number') {
|
|
89
|
+
if (typeof rule.minimum === 'number' && value < rule.minimum) {
|
|
90
|
+
const error = `Invalid paramsSchema: "${key}" must be >= ${rule.minimum}`;
|
|
91
|
+
return { valid: false, error, field: key, code: 'PARAMS_SCHEMA_INVALID' };
|
|
92
|
+
}
|
|
93
|
+
if (typeof rule.maximum === 'number' && value > rule.maximum) {
|
|
94
|
+
const error = `Invalid paramsSchema: "${key}" must be <= ${rule.maximum}`;
|
|
95
|
+
return { valid: false, error, field: key, code: 'PARAMS_SCHEMA_INVALID' };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (typeof value === 'string') {
|
|
99
|
+
if (typeof rule.minLength === 'number' && value.length < rule.minLength) {
|
|
100
|
+
const error = `Invalid paramsSchema: "${key}" is too short`;
|
|
101
|
+
return { valid: false, error, field: key, code: 'PARAMS_SCHEMA_INVALID' };
|
|
102
|
+
}
|
|
103
|
+
if (typeof rule.maxLength === 'number' && value.length > rule.maxLength) {
|
|
104
|
+
const error = `Invalid paramsSchema: "${key}" is too long`;
|
|
105
|
+
return { valid: false, error, field: key, code: 'PARAMS_SCHEMA_INVALID' };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { valid: true };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function sanitizeParamsForSchemaValidation(params) {
|
|
114
|
+
const next = { ...(params && typeof params === 'object' ? params : {}) };
|
|
115
|
+
for (const key of PARAMS_SCHEMA_BILLING_KEYS) {
|
|
116
|
+
delete next[key];
|
|
117
|
+
}
|
|
118
|
+
return next;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function formatParamsSchemaValidationResult(result) {
|
|
122
|
+
if (!result || result.valid) return result;
|
|
123
|
+
return {
|
|
124
|
+
...result,
|
|
125
|
+
field: result.field || extractFieldFromError(result.error),
|
|
126
|
+
};
|
|
127
|
+
}
|