agentex-creator-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/LICENSE +21 -0
- package/README.md +195 -0
- package/dist/packages/contracts/src/deployer-investigation.d.ts +1015 -0
- package/dist/packages/contracts/src/deployer-investigation.js +101 -0
- package/dist/packages/contracts/src/index.d.ts +1701 -0
- package/dist/packages/contracts/src/index.js +380 -0
- package/dist/packages/contracts/src/indexed-activity.d.ts +684 -0
- package/dist/packages/contracts/src/indexed-activity.js +71 -0
- package/dist/packages/contracts/src/indexed-agents.d.ts +299 -0
- package/dist/packages/contracts/src/indexed-agents.js +131 -0
- package/dist/packages/contracts/src/inspection.d.ts +906 -0
- package/dist/packages/contracts/src/inspection.js +114 -0
- package/dist/packages/contracts/src/kinds.d.ts +5398 -0
- package/dist/packages/contracts/src/kinds.js +156 -0
- package/dist/packages/contracts/src/report-presentation.d.ts +346 -0
- package/dist/packages/contracts/src/report-presentation.js +120 -0
- package/dist/packages/contracts/src/solana-inspection.d.ts +451 -0
- package/dist/packages/contracts/src/solana-inspection.js +94 -0
- package/dist/packages/contracts/src/token-market.d.ts +193 -0
- package/dist/packages/contracts/src/token-market.js +335 -0
- package/dist/packages/contracts/src/wallet-analysis.d.ts +866 -0
- package/dist/packages/contracts/src/wallet-analysis.js +89 -0
- package/dist/packages/contracts/src/watchtower.d.ts +1141 -0
- package/dist/packages/contracts/src/watchtower.js +196 -0
- package/dist/packages/contracts/src/workflow.d.ts +1568 -0
- package/dist/packages/contracts/src/workflow.js +651 -0
- package/dist/packages/inspector/src/decode.d.ts +23 -0
- package/dist/packages/inspector/src/decode.js +150 -0
- package/dist/packages/inspector/src/scope.d.ts +87 -0
- package/dist/packages/inspector/src/scope.js +64 -0
- package/dist/packages/model/src/analysis.d.ts +149 -0
- package/dist/packages/model/src/analysis.js +387 -0
- package/dist/packages/model/src/pricing.d.ts +38 -0
- package/dist/packages/model/src/pricing.js +49 -0
- package/dist/packages/model/src/retry.d.ts +20 -0
- package/dist/packages/model/src/retry.js +31 -0
- package/dist/packages/model/src/schema.d.ts +10 -0
- package/dist/packages/model/src/schema.js +51 -0
- package/dist/packages/model/src/summary.d.ts +91 -0
- package/dist/packages/model/src/summary.js +177 -0
- package/dist/packages/model/src/types.d.ts +81 -0
- package/dist/packages/model/src/types.js +19 -0
- package/dist/packages/monitoring/src/delivery.d.ts +32 -0
- package/dist/packages/monitoring/src/delivery.js +53 -0
- package/dist/packages/providers/src/chain-transport.d.ts +42 -0
- package/dist/packages/providers/src/chain-transport.js +57 -0
- package/dist/packages/providers/src/coverage.d.ts +105 -0
- package/dist/packages/providers/src/coverage.js +260 -0
- package/dist/packages/providers/src/health.d.ts +273 -0
- package/dist/packages/providers/src/health.js +505 -0
- package/dist/packages/providers/src/keyed.d.ts +96 -0
- package/dist/packages/providers/src/keyed.js +240 -0
- package/dist/packages/providers/src/snapshot.d.ts +61 -0
- package/dist/packages/providers/src/snapshot.js +77 -0
- package/dist/packages/publication/src/fixtures.d.ts +45 -0
- package/dist/packages/publication/src/fixtures.js +350 -0
- package/dist/packages/research/src/index.d.ts +188 -0
- package/dist/packages/research/src/index.js +829 -0
- package/dist/packages/runtime/src/checkpoints.d.ts +65 -0
- package/dist/packages/runtime/src/checkpoints.js +214 -0
- package/dist/packages/runtime/src/policy.d.ts +57 -0
- package/dist/packages/runtime/src/policy.js +296 -0
- package/dist/packages/sdk/src/bin/agentex-buyer.d.ts +2 -0
- package/dist/packages/sdk/src/bin/agentex-buyer.js +4 -0
- package/dist/packages/sdk/src/bin/agentex.d.ts +2 -0
- package/dist/packages/sdk/src/bin/agentex.js +3 -0
- package/dist/packages/sdk/src/buyer-cli.d.ts +10 -0
- package/dist/packages/sdk/src/buyer-cli.js +210 -0
- package/dist/packages/sdk/src/buyer.d.ts +534 -0
- package/dist/packages/sdk/src/buyer.js +441 -0
- package/dist/packages/sdk/src/cli.d.ts +14 -0
- package/dist/packages/sdk/src/cli.js +149 -0
- package/dist/packages/sdk/src/errors.d.ts +31 -0
- package/dist/packages/sdk/src/errors.js +24 -0
- package/dist/packages/sdk/src/index.d.ts +236 -0
- package/dist/packages/sdk/src/index.js +150 -0
- package/dist/packages/sdk/src/local.d.ts +11 -0
- package/dist/packages/sdk/src/local.js +110 -0
- package/dist/packages/sdk/src/rails.d.ts +59 -0
- package/dist/packages/sdk/src/rails.js +96 -0
- package/dist/packages/sdk/src/report.d.ts +121 -0
- package/dist/packages/sdk/src/report.js +114 -0
- package/dist/packages/sdk/src/version.d.ts +2 -0
- package/dist/packages/sdk/src/version.js +2 -0
- package/dist/packages/watchtower/src/index.d.ts +150 -0
- package/dist/packages/watchtower/src/index.js +786 -0
- package/dist/packages/workflow/src/registry.d.ts +61 -0
- package/dist/packages/workflow/src/registry.js +76 -0
- package/examples/README.md +34 -0
- package/examples/cli-usage.sh +30 -0
- package/examples/fixtures/base-weth-input.json +4 -0
- package/examples/focused-researcher.json +127 -0
- package/examples/pay-with-eth-robinhood.mts +37 -0
- package/examples/pay-with-usdc.mts +41 -0
- package/examples/quickstart.mts +73 -0
- package/package.json +50 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { AgentexApiError } from './errors.js';
|
|
2
|
+
export const USDC_MINT = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
|
|
3
|
+
export const SOL_ASSET_ID = 'solana:mainnet-beta/native';
|
|
4
|
+
export const USDC_ASSET_ID = `solana:mainnet-beta/spl:${USDC_MINT}`;
|
|
5
|
+
export const ETH_ROBINHOOD_ASSET_ID = 'eip155:4663/native';
|
|
6
|
+
/** The production payment assets, SOL first. */
|
|
7
|
+
export const ASSETS = Object.freeze({ SOL: SOL_ASSET_ID, USDC: USDC_ASSET_ID, ETH_ROBINHOOD: ETH_ROBINHOOD_ASSET_ID });
|
|
8
|
+
/** SOL on Solana. */
|
|
9
|
+
export const DEFAULT_ASSET_ID = SOL_ASSET_ID;
|
|
10
|
+
/** Short names accepted wherever an asset id is (`sol`, `usdc`, `eth`). */
|
|
11
|
+
export const ASSET_ALIASES = Object.freeze({ sol: SOL_ASSET_ID, usdc: USDC_ASSET_ID, eth: ETH_ROBINHOOD_ASSET_ID });
|
|
12
|
+
/** Circle's USDC mints (6 decimals). */
|
|
13
|
+
const USDC_MINTS = { 'mainnet-beta': USDC_MINT, devnet: '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU' };
|
|
14
|
+
const SOLANA_NETWORKS = { 'mainnet-beta': 'Solana', devnet: 'Solana devnet', localnet: 'Solana localnet' };
|
|
15
|
+
const EVM_NETWORKS = { 1: 'Ethereum', 8453: 'Base', 4663: 'Robinhood Chain', 46630: 'Robinhood Chain testnet', 31337: 'local test chain' };
|
|
16
|
+
const SOLANA_ID = /^solana:(localnet|devnet|mainnet-beta)\/(native|spl:([1-9A-HJ-NP-Za-km-z]{32,44}))$/;
|
|
17
|
+
const EVM_ID = /^eip155:([1-9][0-9]{0,15})\/(native|erc20:(0x[0-9a-f]{40}))$/;
|
|
18
|
+
const SIM_ID = 'agentex-local-sim:research-rehearsal-v1/fixture';
|
|
19
|
+
const invalidAsset = (value) => new AgentexApiError('invalid_input', null, 'invalid_asset', `"${value.slice(0, 120)}" is not an AGENTEX payment asset id (for example ${SOL_ASSET_ID}) or one of sol, usdc, eth.`, false);
|
|
20
|
+
/** Accepts an asset id or an alias (`sol`, `usdc`, `eth`) and returns the validated asset id. */
|
|
21
|
+
export function resolveAssetId(value) { return describeAsset(value).assetId; }
|
|
22
|
+
/** Parses an asset id (or an alias) into its rail, network, currency and decimals. Throws `invalid_input` for anything else. */
|
|
23
|
+
export function describeAsset(value) {
|
|
24
|
+
const alias = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
25
|
+
const assetId = Object.hasOwn(ASSET_ALIASES, alias) ? ASSET_ALIASES[alias] : typeof value === 'string' ? value : '';
|
|
26
|
+
const solana = SOLANA_ID.exec(assetId);
|
|
27
|
+
if (solana) {
|
|
28
|
+
const cluster = solana[1];
|
|
29
|
+
const mint = solana[3] ?? null;
|
|
30
|
+
const networkLabel = SOLANA_NETWORKS[cluster];
|
|
31
|
+
const usdc = mint !== null && USDC_MINTS[cluster] === mint;
|
|
32
|
+
const currency = mint === null ? 'SOL' : usdc ? 'USDC' : 'SPL';
|
|
33
|
+
return { assetId, rail: 'solana', profile: mint === null ? 'solana_sol' : 'solana_usdc', network: cluster, networkLabel, currency,
|
|
34
|
+
decimals: mint === null ? 9 : usdc ? 6 : null, mint, chainId: null, contract: null, label: `${currency} on ${networkLabel}` };
|
|
35
|
+
}
|
|
36
|
+
const evm = EVM_ID.exec(assetId);
|
|
37
|
+
if (evm) {
|
|
38
|
+
const chainId = Number(evm[1]);
|
|
39
|
+
const contract = evm[3] ?? null;
|
|
40
|
+
const networkLabel = EVM_NETWORKS[chainId] ?? `EVM chain ${chainId}`;
|
|
41
|
+
const currency = contract === null ? 'ETH' : 'ERC-20';
|
|
42
|
+
return { assetId, rail: 'evm', profile: 'local_custody', network: String(chainId), networkLabel, currency, decimals: contract === null ? 18 : null,
|
|
43
|
+
mint: null, chainId, contract, label: `${currency} on ${networkLabel}` };
|
|
44
|
+
}
|
|
45
|
+
if (assetId === SIM_ID)
|
|
46
|
+
return { assetId, rail: 'sim', profile: 'sim', network: 'local-sim', networkLabel: 'local rehearsal', currency: 'SIM', decimals: 0,
|
|
47
|
+
mint: null, chainId: null, contract: null, label: 'SIM fixture units' };
|
|
48
|
+
throw invalidAsset(String(value));
|
|
49
|
+
}
|
|
50
|
+
/** The API key profile that pays in an asset: SOL → `solana_sol`, USDC → `solana_usdc`, ETH → `local_custody`. */
|
|
51
|
+
export function profileForAsset(assetId) { return describeAsset(assetId).profile; }
|
|
52
|
+
/** The quote, acceptance, catalog and billing route prefix for a profile. The Solana rail serves SOL and USDC keys. */
|
|
53
|
+
export function railPath(profile) {
|
|
54
|
+
return profile === 'sim' ? '/v1/rehearsal' : profile === 'local_custody' ? '/v1/custody' : '/v1/solana-custody';
|
|
55
|
+
}
|
|
56
|
+
/** The asset id of a ledger asset object, such as a quote's `asset` (`{ namespace: 'solana', cluster, kind, mint?, decimals }`). */
|
|
57
|
+
export function assetIdOf(asset) {
|
|
58
|
+
if (!asset || typeof asset !== 'object')
|
|
59
|
+
return null;
|
|
60
|
+
const value = asset;
|
|
61
|
+
if (value.namespace === 'solana' && typeof value.cluster === 'string' && (value.kind === 'native' || value.kind === 'spl'))
|
|
62
|
+
return `solana:${value.cluster}/${value.kind}${value.kind === 'spl' && typeof value.mint === 'string' ? `:${value.mint}` : ''}`;
|
|
63
|
+
if (value.namespace === 'eip155' && typeof value.chainId === 'number' && (value.kind === 'native' || value.kind === 'erc20'))
|
|
64
|
+
return `eip155:${value.chainId}/${value.kind}${value.kind === 'erc20' && typeof value.contract === 'string' ? `:${value.contract.toLowerCase()}` : ''}`;
|
|
65
|
+
if (value.namespace === 'agentex-local-sim')
|
|
66
|
+
return SIM_ID;
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const ATOMIC = /^(0|[1-9][0-9]{0,77})$/;
|
|
70
|
+
/** Exact decimal text for an atomic amount: `formatAtomic('2200000', 9)` is `"0.0022"`. No floats, no rounding. */
|
|
71
|
+
export function formatAtomic(amount, decimals) {
|
|
72
|
+
const text = typeof amount === 'bigint' ? amount.toString() : amount;
|
|
73
|
+
if (!ATOMIC.test(text) || !Number.isInteger(decimals) || decimals < 0 || decimals > 36)
|
|
74
|
+
throw new AgentexApiError('invalid_input', null, 'invalid_amount', 'Expected a non-negative integer atomic amount and 0 to 36 decimals.', false);
|
|
75
|
+
if (decimals === 0)
|
|
76
|
+
return text;
|
|
77
|
+
const padded = text.padStart(decimals + 1, '0');
|
|
78
|
+
const whole = padded.slice(0, -decimals);
|
|
79
|
+
const fraction = padded.slice(-decimals).replace(/0+$/, '');
|
|
80
|
+
return fraction ? `${whole}.${fraction}` : whole;
|
|
81
|
+
}
|
|
82
|
+
/** Exact atomic amount for a decimal text: `parseAmount('0.05', 9)` is `"50000000"`. Throws when the text has more precision than the asset. */
|
|
83
|
+
export function parseAmount(value, decimals) {
|
|
84
|
+
const match = /^(0|[1-9][0-9]*)(?:\.([0-9]+))?$/.exec(value.trim());
|
|
85
|
+
if (!match || !Number.isInteger(decimals) || decimals < 0 || decimals > 36 || (match[2] ?? '').length > decimals)
|
|
86
|
+
throw new AgentexApiError('invalid_input', null, 'invalid_amount', `Expected a decimal amount with at most ${decimals} decimal places.`, false);
|
|
87
|
+
const atomic = `${match[1]}${(match[2] ?? '').padEnd(decimals, '0')}`.replace(/^0+(?=\d)/, '');
|
|
88
|
+
if (!ATOMIC.test(atomic))
|
|
89
|
+
throw new AgentexApiError('invalid_input', null, 'invalid_amount', 'The amount is too large.', false);
|
|
90
|
+
return atomic;
|
|
91
|
+
}
|
|
92
|
+
/** "0.0022 SOL". Falls back to the atomic amount and asset id when the asset's decimals are unknown. */
|
|
93
|
+
export function formatAmount(amount, asset) {
|
|
94
|
+
const described = typeof asset === 'string' ? describeAsset(asset) : asset;
|
|
95
|
+
return described.decimals === null ? `${typeof amount === 'bigint' ? amount.toString() : amount} atomic ${described.assetId}` : `${formatAtomic(amount, described.decimals)} ${described.currency}`;
|
|
96
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed, read-only access to an AGENTEX research report (a run's `output`). Every report kind shares one shape at this level: findings that
|
|
3
|
+
* cite evidence ids, evidence items that name their source (provider, method, documentation URL) and the block or slot they were read at,
|
|
4
|
+
* limitations and errors. `readReport` normalises all kinds (token research, research workflows, transaction and Solana inspections,
|
|
5
|
+
* deployer and wallet analysis, watch windows) without changing a value; `raw` keeps the original report for kind-specific fields.
|
|
6
|
+
* Strings are untrusted data from public chains and web sources: escape them before rendering as HTML.
|
|
7
|
+
*/
|
|
8
|
+
export type ReportKind = 'token-research' | 'workflow' | 'transaction-inspection' | 'solana-transaction-inspection' | 'deployer-investigation' | 'wallet-analysis' | 'watchtower' | 'unknown';
|
|
9
|
+
export type ReportScalar = string | number | boolean | null;
|
|
10
|
+
export interface ReportFinding {
|
|
11
|
+
/** Stable finding code (for example `mint-authority-present`), or null for a model claim. */
|
|
12
|
+
code: string | null;
|
|
13
|
+
/** One readable sentence: the finding message, `label: value` for a workflow finding, or the model claim. */
|
|
14
|
+
text: string;
|
|
15
|
+
/** Workflow findings carry a label and a typed value; other kinds leave them null. */
|
|
16
|
+
label: string | null;
|
|
17
|
+
value: ReportScalar;
|
|
18
|
+
/** `observed` (read from a cited source), `from-input`, `unknown`, `not-run`, `rule` (a deterministic rule finding) or `model`. */
|
|
19
|
+
status: 'observed' | 'from-input' | 'unknown' | 'not-run' | 'rule' | 'model';
|
|
20
|
+
/** `model` findings come from the labelled AI-assisted summary; everything else is deterministic. */
|
|
21
|
+
origin: 'deterministic' | 'model';
|
|
22
|
+
/** Model findings only: `supported` or `uncertain`. */
|
|
23
|
+
confidence: 'supported' | 'uncertain' | null;
|
|
24
|
+
/** Workflow section id, `analysis` for model findings, `signals` for tool signals, otherwise null. */
|
|
25
|
+
section: string | null;
|
|
26
|
+
/** Why an `unknown` or `not-run` finding has no value. */
|
|
27
|
+
reason: string | null;
|
|
28
|
+
evidenceIds: string[];
|
|
29
|
+
}
|
|
30
|
+
export interface ReportEvidenceSource {
|
|
31
|
+
kind: string;
|
|
32
|
+
provider: string;
|
|
33
|
+
method: string;
|
|
34
|
+
documentationUrl: string | null;
|
|
35
|
+
}
|
|
36
|
+
export interface ReportEvidence {
|
|
37
|
+
id: string;
|
|
38
|
+
tool: string | null;
|
|
39
|
+
source: ReportEvidenceSource;
|
|
40
|
+
chain: string | null;
|
|
41
|
+
/** The pinned block (EVM) or slot (Solana) the item was read at, when it has one. */
|
|
42
|
+
block: string | null;
|
|
43
|
+
slot: string | null;
|
|
44
|
+
blockHash: string | null;
|
|
45
|
+
observedAt: string | null;
|
|
46
|
+
finality: string | null;
|
|
47
|
+
canonicality: string | null;
|
|
48
|
+
completeness: string | null;
|
|
49
|
+
trust: string | null;
|
|
50
|
+
request: unknown;
|
|
51
|
+
result: unknown;
|
|
52
|
+
contentHash: string | null;
|
|
53
|
+
}
|
|
54
|
+
/** One provider and source kind, with every method read from it and the evidence items it produced. */
|
|
55
|
+
export interface ReportSource {
|
|
56
|
+
key: string;
|
|
57
|
+
kind: string;
|
|
58
|
+
provider: string;
|
|
59
|
+
methods: string[];
|
|
60
|
+
documentationUrl: string | null;
|
|
61
|
+
evidenceIds: string[];
|
|
62
|
+
}
|
|
63
|
+
export interface ReportSection {
|
|
64
|
+
id: string;
|
|
65
|
+
title: string;
|
|
66
|
+
status: string | null;
|
|
67
|
+
reason: string | null;
|
|
68
|
+
findings: ReportFinding[];
|
|
69
|
+
}
|
|
70
|
+
export interface ReportError {
|
|
71
|
+
category: string | null;
|
|
72
|
+
message: string;
|
|
73
|
+
step: string | null;
|
|
74
|
+
evidenceId: string | null;
|
|
75
|
+
}
|
|
76
|
+
export interface ResearchReport {
|
|
77
|
+
schemaVersion: string | null;
|
|
78
|
+
kind: ReportKind;
|
|
79
|
+
status: 'complete' | 'partial' | 'failed' | null;
|
|
80
|
+
generatedAt: string | null;
|
|
81
|
+
agentVersion: string | null;
|
|
82
|
+
manifestHash: string | null;
|
|
83
|
+
/** The result in one sentence, when the report declares one (workflow presentations). */
|
|
84
|
+
headline: string | null;
|
|
85
|
+
/** Deterministic findings first, then AI-assisted claims (`origin: 'model'`). */
|
|
86
|
+
findings: ReportFinding[];
|
|
87
|
+
/** Workflow reports: the declared sections with their typed findings. Empty for other kinds. */
|
|
88
|
+
sections: ReportSection[];
|
|
89
|
+
evidence: ReportEvidence[];
|
|
90
|
+
sources: ReportSource[];
|
|
91
|
+
limitations: string[];
|
|
92
|
+
unknowns: string[];
|
|
93
|
+
errors: ReportError[];
|
|
94
|
+
analysis: {
|
|
95
|
+
status: string;
|
|
96
|
+
model: string | null;
|
|
97
|
+
unavailableReason: string | null;
|
|
98
|
+
} | null;
|
|
99
|
+
snapshot: {
|
|
100
|
+
block: string | null;
|
|
101
|
+
blockHash: string | null;
|
|
102
|
+
finality: string | null;
|
|
103
|
+
canonicality: string | null;
|
|
104
|
+
} | null;
|
|
105
|
+
usage: {
|
|
106
|
+
rpcCalls: number | null;
|
|
107
|
+
maximumRpcCalls: number | null;
|
|
108
|
+
elapsedMs: number | null;
|
|
109
|
+
modelTokens: number | null;
|
|
110
|
+
};
|
|
111
|
+
/** The report exactly as stored. */
|
|
112
|
+
raw: Record<string, unknown>;
|
|
113
|
+
}
|
|
114
|
+
/** Groups evidence by source kind and provider, keeping first-seen order. */
|
|
115
|
+
export declare function sourcesOf(evidence: readonly ReportEvidence[]): ReportSource[];
|
|
116
|
+
/** The report kind named by a stored report's `schemaVersion`. */
|
|
117
|
+
export declare function reportKind(output: unknown): ReportKind;
|
|
118
|
+
/** Normalises a run's `output`. A null or non-object output reads as an empty `unknown` report rather than throwing. */
|
|
119
|
+
export declare function readReport(output: unknown): ResearchReport;
|
|
120
|
+
/** The evidence items a finding (or a list of evidence ids) cites, in citation order. Unknown ids are skipped. */
|
|
121
|
+
export declare function evidenceFor(report: Pick<ResearchReport, 'evidence'>, cited: Pick<ReportFinding, 'evidenceIds'> | readonly string[]): ReportEvidence[];
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const SCHEMA_KINDS = {
|
|
2
|
+
'agentex.token-research.v1': 'token-research', 'agentex.workflow.v1': 'workflow', 'agentex.transaction-inspection.v1': 'transaction-inspection',
|
|
3
|
+
'agentex.solana-transaction-inspection.v1': 'solana-transaction-inspection', 'agentex.deployer-investigation.v1': 'deployer-investigation',
|
|
4
|
+
'agentex.wallet-analysis.v1': 'wallet-analysis', 'agentex.watchtower-window.v1': 'watchtower',
|
|
5
|
+
};
|
|
6
|
+
const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
7
|
+
const list = (value) => Array.isArray(value) ? value : [];
|
|
8
|
+
const text = (value) => typeof value === 'string' ? value : typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint' ? String(value) : '';
|
|
9
|
+
const nullable = (value) => text(value) || null;
|
|
10
|
+
const ids = (value) => list(value).filter((item) => typeof item === 'string');
|
|
11
|
+
const scalar = (value) => typeof value === 'string' || typeof value === 'boolean' || (typeof value === 'number' && Number.isFinite(value)) ? value : null;
|
|
12
|
+
const count = (value) => typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
13
|
+
const strings = (value) => list(value).map(item => typeof item === 'string' ? item : text(record(item)?.message) || text(record(item)?.reason)).filter(Boolean);
|
|
14
|
+
function evidenceOf(report) {
|
|
15
|
+
return list(report.evidence).flatMap((raw) => {
|
|
16
|
+
const item = record(raw);
|
|
17
|
+
const id = text(item?.id);
|
|
18
|
+
if (!item || !id)
|
|
19
|
+
return [];
|
|
20
|
+
const source = record(item.source);
|
|
21
|
+
const snapshot = record(item.snapshot);
|
|
22
|
+
return [{ id, tool: nullable(item.tool), source: { kind: text(source?.kind) || 'rpc', provider: text(source?.provider) || 'unknown', method: text(source?.method) || 'read', documentationUrl: nullable(source?.documentationUrl) },
|
|
23
|
+
chain: nullable(item.chain), block: nullable(snapshot?.blockNumber), slot: nullable(snapshot?.slot), blockHash: nullable(snapshot?.blockHash),
|
|
24
|
+
observedAt: nullable(item.observedAt), finality: nullable(item.finality), canonicality: nullable(item.canonicality), completeness: nullable(item.completeness), trust: nullable(item.trust),
|
|
25
|
+
request: item.request, result: item.result, contentHash: nullable(item.contentHash) }];
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/** Groups evidence by source kind and provider, keeping first-seen order. */
|
|
29
|
+
export function sourcesOf(evidence) {
|
|
30
|
+
const map = new Map();
|
|
31
|
+
for (const item of evidence) {
|
|
32
|
+
const key = `${item.source.kind}:${item.source.provider}`;
|
|
33
|
+
const entry = map.get(key) ?? { key, kind: item.source.kind, provider: item.source.provider, methods: [], documentationUrl: item.source.documentationUrl, evidenceIds: [] };
|
|
34
|
+
if (!entry.methods.includes(item.source.method))
|
|
35
|
+
entry.methods.push(item.source.method);
|
|
36
|
+
entry.documentationUrl ??= item.source.documentationUrl;
|
|
37
|
+
entry.evidenceIds.push(item.id);
|
|
38
|
+
map.set(key, entry);
|
|
39
|
+
}
|
|
40
|
+
return [...map.values()];
|
|
41
|
+
}
|
|
42
|
+
function workflowFinding(raw, section) {
|
|
43
|
+
const item = record(raw);
|
|
44
|
+
if (!item)
|
|
45
|
+
return null;
|
|
46
|
+
const status = text(item.status);
|
|
47
|
+
const label = text(item.label) || text(item.code);
|
|
48
|
+
const value = scalar(item.value);
|
|
49
|
+
const known = status === 'observed' || status === 'from-input';
|
|
50
|
+
return { code: nullable(item.code), text: known ? `${label}: ${value === null ? '' : String(value)}` : `${label}: ${status || 'unknown'}${text(item.reason) ? ` (${text(item.reason)})` : ''}`,
|
|
51
|
+
label: label || null, value: known ? value : null, status: (['observed', 'from-input', 'unknown', 'not-run'].includes(status) ? status : 'unknown'),
|
|
52
|
+
origin: 'deterministic', confidence: null, section, reason: nullable(item.reason), evidenceIds: ids(item.evidenceIds) };
|
|
53
|
+
}
|
|
54
|
+
/** The report kind named by a stored report's `schemaVersion`. */
|
|
55
|
+
export function reportKind(output) { return SCHEMA_KINDS[text(record(output)?.schemaVersion)] ?? 'unknown'; }
|
|
56
|
+
/** Normalises a run's `output`. A null or non-object output reads as an empty `unknown` report rather than throwing. */
|
|
57
|
+
export function readReport(output) {
|
|
58
|
+
const report = record(output) ?? {};
|
|
59
|
+
const kind = reportKind(report);
|
|
60
|
+
const evidence = evidenceOf(report);
|
|
61
|
+
const findings = [];
|
|
62
|
+
const rule = (raw, section = null) => {
|
|
63
|
+
const item = record(raw);
|
|
64
|
+
if (!item || !text(item.message))
|
|
65
|
+
return;
|
|
66
|
+
findings.push({ code: nullable(item.code), text: text(item.message), label: null, value: null, status: 'rule', origin: 'deterministic', confidence: null, section, reason: null, evidenceIds: ids(item.evidenceIds) });
|
|
67
|
+
};
|
|
68
|
+
for (const raw of list(report.findings))
|
|
69
|
+
rule(raw);
|
|
70
|
+
const sections = list(report.sections).flatMap((raw) => {
|
|
71
|
+
const section = record(raw);
|
|
72
|
+
const id = text(section?.id);
|
|
73
|
+
if (!section || !id)
|
|
74
|
+
return [];
|
|
75
|
+
return [{ id, title: text(section.title) || id, status: nullable(section.status), reason: nullable(section.reason),
|
|
76
|
+
findings: list(section.findings).flatMap(item => { const finding = workflowFinding(item, id); return finding ? [finding] : []; }) }];
|
|
77
|
+
});
|
|
78
|
+
for (const section of sections)
|
|
79
|
+
findings.push(...section.findings);
|
|
80
|
+
for (const raw of list(report.signals))
|
|
81
|
+
rule(raw, 'signals');
|
|
82
|
+
const presentation = record(report.presentation);
|
|
83
|
+
const headlineRef = record(presentation?.headline);
|
|
84
|
+
const headlineFinding = headlineRef ? sections.find(section => section.id === text(headlineRef.section))?.findings.find(item => item.code === text(headlineRef.finding) && item.status === 'observed') : undefined;
|
|
85
|
+
const analysis = record(report.analysis);
|
|
86
|
+
if (analysis?.status === 'completed')
|
|
87
|
+
for (const raw of list(analysis.findings)) {
|
|
88
|
+
const item = record(raw);
|
|
89
|
+
if (!item || !text(item.claim))
|
|
90
|
+
continue;
|
|
91
|
+
findings.push({ code: null, text: text(item.claim), label: null, value: null, status: 'model', origin: 'model', confidence: item.confidence === 'uncertain' ? 'uncertain' : 'supported', section: 'analysis', reason: null, evidenceIds: ids(item.evidenceIds) });
|
|
92
|
+
}
|
|
93
|
+
const status = text(report.status);
|
|
94
|
+
const snapshot = record(report.snapshot);
|
|
95
|
+
const usage = record(report.usage);
|
|
96
|
+
return {
|
|
97
|
+
schemaVersion: nullable(report.schemaVersion), kind, status: status === 'complete' || status === 'partial' || status === 'failed' ? status : null,
|
|
98
|
+
generatedAt: nullable(report.generatedAt), agentVersion: nullable(report.agentVersion), manifestHash: nullable(report.manifestHash),
|
|
99
|
+
headline: headlineFinding && headlineFinding.value !== null ? String(headlineFinding.value) : null,
|
|
100
|
+
findings, sections, evidence, sources: sourcesOf(evidence),
|
|
101
|
+
limitations: strings(report.limitations), unknowns: strings(report.unknowns),
|
|
102
|
+
errors: list(report.errors).flatMap((raw) => { const item = record(raw); return item && text(item.message) ? [{ category: nullable(item.category), message: text(item.message), step: nullable(item.step), evidenceId: nullable(item.evidenceId) }] : []; }),
|
|
103
|
+
analysis: analysis ? { status: text(analysis.status), model: nullable(analysis.model), unavailableReason: nullable(analysis.unavailableReason) } : null,
|
|
104
|
+
snapshot: snapshot ? { block: nullable(snapshot.blockNumber) ?? nullable(snapshot.slot), blockHash: nullable(snapshot.blockHash), finality: nullable(snapshot.finality), canonicality: nullable(snapshot.canonicality) } : null,
|
|
105
|
+
usage: { rpcCalls: count(usage?.rpcCalls), maximumRpcCalls: count(usage?.maximumRpcCalls), elapsedMs: count(usage?.elapsedMs), modelTokens: count(usage?.modelTokens) },
|
|
106
|
+
raw: report,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/** The evidence items a finding (or a list of evidence ids) cites, in citation order. Unknown ids are skipped. */
|
|
110
|
+
export function evidenceFor(report, cited) {
|
|
111
|
+
const byId = new Map(report.evidence.map(item => [item.id, item]));
|
|
112
|
+
const wanted = Array.isArray(cited) ? cited : cited.evidenceIds;
|
|
113
|
+
return wanted.flatMap(id => { const item = byId.get(id); return item ? [item] : []; });
|
|
114
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { type Hex } from 'viem';
|
|
2
|
+
import { type AgentManifest } from '../../contracts/src/index.js';
|
|
3
|
+
import { CONTROL_EVENTS, GOVERNANCE_EVENTS, type WatchEvent, type WatchEvidenceItem, type WatchIgnoredReason, type WatchRule, type WatchtowerWindowInput, type WatchtowerWindowReport } from '../../contracts/src/watchtower.js';
|
|
4
|
+
import { type RpcTransport } from '../../research/src/index.js';
|
|
5
|
+
type Environment = Record<string, string | undefined>;
|
|
6
|
+
export declare const WATCH_TOPICS: {
|
|
7
|
+
readonly Transfer: `0x${string}`;
|
|
8
|
+
readonly Approval: `0x${string}`;
|
|
9
|
+
readonly Upgraded: `0x${string}`;
|
|
10
|
+
readonly AdminChanged: `0x${string}`;
|
|
11
|
+
readonly BeaconUpgraded: `0x${string}`;
|
|
12
|
+
readonly OwnershipTransferred: `0x${string}`;
|
|
13
|
+
};
|
|
14
|
+
/** 0.3.0 pool events. Uniswap V2 forks, Uniswap V3 forks and Aerodrome (Solidly) pools use different Swap and Burn layouts. */
|
|
15
|
+
export declare const POOL_TOPICS: {
|
|
16
|
+
readonly 'uniswap-v2': {
|
|
17
|
+
readonly Swap: `0x${string}`;
|
|
18
|
+
readonly Mint: `0x${string}`;
|
|
19
|
+
readonly Burn: `0x${string}`;
|
|
20
|
+
};
|
|
21
|
+
readonly 'uniswap-v3': {
|
|
22
|
+
readonly Swap: `0x${string}`;
|
|
23
|
+
readonly Mint: `0x${string}`;
|
|
24
|
+
readonly Burn: `0x${string}`;
|
|
25
|
+
};
|
|
26
|
+
readonly aerodrome: {
|
|
27
|
+
readonly Swap: `0x${string}`;
|
|
28
|
+
readonly Mint: `0x${string}`;
|
|
29
|
+
readonly Burn: `0x${string}`;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
/** 0.3.0 governor, OpenZeppelin TimelockController and Compound Timelock events. */
|
|
33
|
+
export declare const GOVERNANCE_TOPICS: Readonly<Record<typeof GOVERNANCE_EVENTS[number], string>>;
|
|
34
|
+
/** 0.3.0 OpenZeppelin AccessControl and Pausable events. */
|
|
35
|
+
export declare const CONTROL_TOPICS: Readonly<Record<typeof CONTROL_EVENTS[number], string>>;
|
|
36
|
+
/** Names for widely used AccessControl role ids, used only to explain an event. An unknown role is shown by its id. */
|
|
37
|
+
export declare const KNOWN_ROLES: Readonly<Record<string, string>>;
|
|
38
|
+
export interface WatchOptions {
|
|
39
|
+
env?: Environment;
|
|
40
|
+
manifest?: AgentManifest;
|
|
41
|
+
signal?: AbortSignal;
|
|
42
|
+
onProgress?: (event: {
|
|
43
|
+
step: string;
|
|
44
|
+
message: string;
|
|
45
|
+
}) => void | Promise<void>;
|
|
46
|
+
/** Durable workers atomically reserve each read against the run budget and fencing token, exactly as for the other kinds. */
|
|
47
|
+
/** The result is ignored: this tool makes no hidden provider attempts, so it never settles a permit itself. */
|
|
48
|
+
beforeRpc?: (operation: {
|
|
49
|
+
method: string;
|
|
50
|
+
step: string;
|
|
51
|
+
maximumRpcCalls: number;
|
|
52
|
+
}) => unknown;
|
|
53
|
+
onEvidence?: (evidence: WatchEvidenceItem) => void | Promise<void>;
|
|
54
|
+
restore?: {
|
|
55
|
+
manifestHash: string;
|
|
56
|
+
inputHash: string;
|
|
57
|
+
evidence: WatchEvidenceItem[];
|
|
58
|
+
};
|
|
59
|
+
/** Server-only fixture transport and clock. */
|
|
60
|
+
transport?: RpcTransport;
|
|
61
|
+
now?: () => Date;
|
|
62
|
+
/**
|
|
63
|
+
* R01: eligible public reads shared across runs and monitors. Only the raw public `eth_getLogs` response for an exact chain id, filter and
|
|
64
|
+
* verified window-edge hashes is shared; events, notifications, deliveries and billing stay with each run's own monitor.
|
|
65
|
+
*/
|
|
66
|
+
sharedReads?: SharedReadCache;
|
|
67
|
+
}
|
|
68
|
+
/** Identity of one shareable log read. The key binds chain id, the exact filter and both window-edge block hashes, so a reorged window never matches. */
|
|
69
|
+
export interface SharedReadKey {
|
|
70
|
+
key: string;
|
|
71
|
+
chain: string;
|
|
72
|
+
chainId: number;
|
|
73
|
+
fromBlock: string;
|
|
74
|
+
toBlock: string;
|
|
75
|
+
fromBlockHash: string;
|
|
76
|
+
toBlockHash: string;
|
|
77
|
+
address: string;
|
|
78
|
+
topics: unknown;
|
|
79
|
+
}
|
|
80
|
+
export interface SharedReadCache {
|
|
81
|
+
get(key: SharedReadKey): Promise<{
|
|
82
|
+
result: unknown;
|
|
83
|
+
observedAt: string;
|
|
84
|
+
} | null>;
|
|
85
|
+
put(key: SharedReadKey, value: {
|
|
86
|
+
result: unknown;
|
|
87
|
+
observedAt: string;
|
|
88
|
+
logs: number;
|
|
89
|
+
}): Promise<void>;
|
|
90
|
+
}
|
|
91
|
+
export declare const SHARED_READ_PROVIDER_SUFFIX = "-shared-public-read";
|
|
92
|
+
export declare function sharedReadKey(chain: string, chainId: number, rule: WatchRule, input: Pick<WatchtowerWindowInput, 'fromBlock' | 'toBlock'>, fromBlockHash: string, toBlockHash: string): SharedReadKey;
|
|
93
|
+
export interface RpcRequest {
|
|
94
|
+
method: string;
|
|
95
|
+
params: unknown[];
|
|
96
|
+
}
|
|
97
|
+
export declare const blockTag: (block: bigint | string) => string;
|
|
98
|
+
/** The exact, deterministic eth_getLogs filter for a rule. Receipt validation checks saved requests against these filters. */
|
|
99
|
+
export declare function logFilter(rule: WatchRule, input: Pick<WatchtowerWindowInput, 'fromBlock' | 'toBlock'>): {
|
|
100
|
+
address: string | string[];
|
|
101
|
+
fromBlock: string;
|
|
102
|
+
toBlock: string;
|
|
103
|
+
topics: (string | string[] | null)[];
|
|
104
|
+
};
|
|
105
|
+
export declare const watchRequests: {
|
|
106
|
+
logs: (rule: WatchRule, input: WatchtowerWindowInput) => RpcRequest;
|
|
107
|
+
header: (tag: string) => RpcRequest;
|
|
108
|
+
};
|
|
109
|
+
export declare const requestKey: (request: {
|
|
110
|
+
method: string;
|
|
111
|
+
params: readonly unknown[];
|
|
112
|
+
}) => string;
|
|
113
|
+
/** Method, request and snapshot scope for a Watchtower receipt. Schema, chain identity and hash checks stay with the caller. */
|
|
114
|
+
export declare function watchReceiptInScope(receipt: WatchEvidenceItem, input: WatchtowerWindowInput): boolean;
|
|
115
|
+
export interface ParsedLog {
|
|
116
|
+
address: string;
|
|
117
|
+
topics: string[];
|
|
118
|
+
data: Hex;
|
|
119
|
+
blockNumber: bigint;
|
|
120
|
+
blockHash: string;
|
|
121
|
+
transactionHash: string;
|
|
122
|
+
logIndex: number;
|
|
123
|
+
removed: boolean;
|
|
124
|
+
raw: unknown;
|
|
125
|
+
}
|
|
126
|
+
export declare function parseLogs(raw: unknown, input: Pick<WatchtowerWindowInput, 'fromBlock' | 'toBlock'>): ParsedLog[];
|
|
127
|
+
/** Keeps whole blocks only: if the cap falls inside a block, that block and later blocks are reported as not examined. */
|
|
128
|
+
export declare function capLogs(logs: ParsedLog[], capacity: number, input: Pick<WatchtowerWindowInput, 'fromBlock' | 'toBlock'>): {
|
|
129
|
+
kept: ParsedLog[];
|
|
130
|
+
coveredThrough: bigint;
|
|
131
|
+
truncated: boolean;
|
|
132
|
+
};
|
|
133
|
+
export type LogMatch = {
|
|
134
|
+
fired: true;
|
|
135
|
+
event: WatchEvent['event'];
|
|
136
|
+
fields: Record<string, string>;
|
|
137
|
+
explanation: string;
|
|
138
|
+
} | {
|
|
139
|
+
fired: false;
|
|
140
|
+
reason: WatchIgnoredReason;
|
|
141
|
+
};
|
|
142
|
+
export declare function matchLog(rule: WatchRule, log: ParsedLog): LogMatch;
|
|
143
|
+
export declare const eventKey: (chain: string, transactionHash: string, logIndex: number, ruleId: string) => string;
|
|
144
|
+
export declare function createWatchTransport(url: string): RpcTransport;
|
|
145
|
+
/**
|
|
146
|
+
* Read-only scan of one window at or below the latest head (instant reads; no finality wait). Truncation, provider failure and reorgs
|
|
147
|
+
* are reported; none becomes "nothing happened". A reorg of the window end fails the window, and monitors retract and redo it.
|
|
148
|
+
*/
|
|
149
|
+
export declare function watchWindow(rawInput: unknown, options?: WatchOptions): Promise<WatchtowerWindowReport>;
|
|
150
|
+
export {};
|