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,65 @@
|
|
|
1
|
+
import { type EvidenceItem, type ResearchInput } from '../../contracts/src/index.js';
|
|
2
|
+
import { type InspectionEvidenceItem, type TransactionInspectionInput } from '../../contracts/src/inspection.js';
|
|
3
|
+
import { type AnyEvidenceItem, type RunInput } from '../../contracts/src/kinds.js';
|
|
4
|
+
import { type SolanaTransactionInspectionInput } from '../../contracts/src/solana-inspection.js';
|
|
5
|
+
import { type ToolEvidenceItem } from '../../contracts/src/workflow.js';
|
|
6
|
+
import { type WatchEvidenceItem, type WatchtowerWindowInput } from '../../contracts/src/watchtower.js';
|
|
7
|
+
export declare class CheckpointIntegrityError extends Error {
|
|
8
|
+
readonly code = "CHECKPOINT_INTEGRITY_FAILURE";
|
|
9
|
+
constructor();
|
|
10
|
+
}
|
|
11
|
+
export interface CheckpointRow {
|
|
12
|
+
id: string;
|
|
13
|
+
run_id: string;
|
|
14
|
+
version_id: string;
|
|
15
|
+
manifest_hash: string;
|
|
16
|
+
input_hash: string;
|
|
17
|
+
operation: number;
|
|
18
|
+
source_fence: number;
|
|
19
|
+
evidence: AnyEvidenceItem;
|
|
20
|
+
checkpoint_hash: string;
|
|
21
|
+
created_at: Date;
|
|
22
|
+
}
|
|
23
|
+
/** A stored AI analysis node result (G02), restored only after `restoreWorkflowAnalysis` re-validates it against the current run. */
|
|
24
|
+
export interface StoredAnalysisResult {
|
|
25
|
+
nodeId: string;
|
|
26
|
+
stored: {
|
|
27
|
+
checkpointKey: string;
|
|
28
|
+
value: unknown;
|
|
29
|
+
evidenceIds: string[];
|
|
30
|
+
usage: {
|
|
31
|
+
modelTokens: number;
|
|
32
|
+
costMicrousd: number;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export interface RecoveryBundle<Evidence extends AnyEvidenceItem = EvidenceItem> {
|
|
37
|
+
manifestHash: string;
|
|
38
|
+
inputHash: string;
|
|
39
|
+
evidence: Evidence[];
|
|
40
|
+
analysis?: StoredAnalysisResult[];
|
|
41
|
+
}
|
|
42
|
+
/** `manifest` (the pinned version) lets workflow receipts be checked against declared tools; other kinds ignore it. */
|
|
43
|
+
export interface CheckpointContract<Input extends RunInput = RunInput> {
|
|
44
|
+
id: string;
|
|
45
|
+
version_id: string;
|
|
46
|
+
manifest_hash: string;
|
|
47
|
+
input: Input;
|
|
48
|
+
rpc_used: number;
|
|
49
|
+
fence: number;
|
|
50
|
+
manifest?: unknown;
|
|
51
|
+
}
|
|
52
|
+
export declare function checkpointHash(row: Omit<CheckpointRow, 'id' | 'checkpoint_hash' | 'created_at'>): string;
|
|
53
|
+
/** Check source scope independently of the executor before saving or reusing a captured response. */
|
|
54
|
+
export declare function validateReceipt(evidence: unknown, input: ResearchInput): EvidenceItem;
|
|
55
|
+
/** Transaction Inspector receipts may only contain reads derivable from the accepted payload (sender, target and decoded grantees). */
|
|
56
|
+
export declare function validateInspectionReceipt(evidence: unknown, input: TransactionInspectionInput): InspectionEvidenceItem;
|
|
57
|
+
/** Watchtower receipts may only contain identity, window headers and the exact log filters derived from the accepted rules. */
|
|
58
|
+
export declare function validateWatchReceipt(evidence: unknown, input: WatchtowerWindowInput): WatchEvidenceItem;
|
|
59
|
+
/** Solana inspector receipts: the declared tool, read-only allowlisted methods, the accepted cluster, and private simulation payloads. */
|
|
60
|
+
export declare function validateSolanaInspectionReceipt(evidence: unknown, input: SolanaTransactionInspectionInput): ToolEvidenceItem;
|
|
61
|
+
/** Kind dispatch on the accepted input shape; the worker separately checks that the input kind matches the pinned manifest kind. */
|
|
62
|
+
export declare function validateRunReceipt(evidence: unknown, input: RunInput, manifest?: unknown): AnyEvidenceItem;
|
|
63
|
+
/** Checks the entire envelope, not only the result's checksum. A corrupt cache is never a cache miss. */
|
|
64
|
+
export declare function restoreCheckpoints(run: CheckpointContract<ResearchInput>, rows: CheckpointRow[]): RecoveryBundle<EvidenceItem>;
|
|
65
|
+
export declare function restoreCheckpoints(run: CheckpointContract, rows: CheckpointRow[]): RecoveryBundle<AnyEvidenceItem>;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { contentHash, EvidenceItemSchema } from '../../contracts/src/index.js';
|
|
2
|
+
import { InspectionEvidenceItemSchema } from '../../contracts/src/inspection.js';
|
|
3
|
+
import { runCoverageChain, runInputKind } from '../../contracts/src/kinds.js';
|
|
4
|
+
import { SOLANA_INSPECTION_RPC_METHODS, SOLANA_INSPECTION_TOOL } from '../../contracts/src/solana-inspection.js';
|
|
5
|
+
import { ToolEvidenceItemSchema } from '../../contracts/src/workflow.js';
|
|
6
|
+
import { validateWorkflowReceipt } from './policy.js';
|
|
7
|
+
import { inspectionReceiptInScope } from '../../inspector/src/scope.js';
|
|
8
|
+
import { WatchEvidenceItemSchema } from '../../contracts/src/watchtower.js';
|
|
9
|
+
import { watchReceiptInScope } from '../../watchtower/src/index.js';
|
|
10
|
+
import { validateDeployerReceipt } from '../../contracts/src/deployer-investigation.js';
|
|
11
|
+
import { validateWalletReceipt } from '../../contracts/src/wallet-analysis.js';
|
|
12
|
+
import { HOLDER_ACTIVITY_METHOD, marketCallInScope, marketTransferRequestInScope } from '../../contracts/src/token-market.js';
|
|
13
|
+
export class CheckpointIntegrityError extends Error {
|
|
14
|
+
code = 'CHECKPOINT_INTEGRITY_FAILURE';
|
|
15
|
+
constructor() { super('A saved research checkpoint failed integrity or run-contract validation.'); }
|
|
16
|
+
}
|
|
17
|
+
const STATE_METHODS = ['eth_getCode', 'eth_getStorageAt', 'eth_call', 'eth_estimateGas', 'eth_getLogs'];
|
|
18
|
+
export function checkpointHash(row) {
|
|
19
|
+
return contentHash({ runId: row.run_id, versionId: row.version_id, manifestHash: row.manifest_hash, inputHash: row.input_hash,
|
|
20
|
+
operation: row.operation, sourceFence: row.source_fence, evidence: row.evidence });
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The block parameter of a Token Researcher state read: the snapshot's hex block number, or, on Robinhood Chain (pinned by hash, EIP-1898),
|
|
24
|
+
* exactly `{ blockHash: <snapshot hash>, requireCanonical: true }` (packages/providers snapshotBlockTag).
|
|
25
|
+
*/
|
|
26
|
+
function pinnedBlockParameter(value, snapshot, chain) {
|
|
27
|
+
if (value === `0x${BigInt(snapshot.blockNumber).toString(16)}`)
|
|
28
|
+
return true;
|
|
29
|
+
if (chain !== 'robinhood' || value === null || typeof value !== 'object' || Array.isArray(value))
|
|
30
|
+
return false;
|
|
31
|
+
const tag = value;
|
|
32
|
+
return Object.keys(tag).length === 2 && tag.requireCanonical === true && typeof tag.blockHash === 'string' && tag.blockHash.toLowerCase() === snapshot.blockHash.toLowerCase();
|
|
33
|
+
}
|
|
34
|
+
function chainIdentityMatches(receipt, chain) {
|
|
35
|
+
return receipt.chain === chain && contentHash(receipt.result) === receipt.contentHash && !((chain === 'ethereum' && receipt.chainId !== 1) || (chain === 'base' && receipt.chainId !== 8453)
|
|
36
|
+
|| (chain === 'robinhood' && ![4663, 46630].includes(receipt.chainId)));
|
|
37
|
+
}
|
|
38
|
+
/** Check source scope independently of the executor before saving or reusing a captured response. */
|
|
39
|
+
export function validateReceipt(evidence, input) {
|
|
40
|
+
try {
|
|
41
|
+
const receipt = EvidenceItemSchema.parse(evidence);
|
|
42
|
+
if (!chainIdentityMatches(receipt, input.chain))
|
|
43
|
+
throw new CheckpointIntegrityError();
|
|
44
|
+
const method = receipt.source.method;
|
|
45
|
+
const params = receipt.request;
|
|
46
|
+
if (method === 'eth_chainId') {
|
|
47
|
+
if (params.length !== 0 || receipt.snapshot !== null)
|
|
48
|
+
throw new CheckpointIntegrityError();
|
|
49
|
+
}
|
|
50
|
+
else if (method === 'eth_getBlockByNumber') {
|
|
51
|
+
// `latest` is the instant-read head; `finalized` and `safe` only admit receipts checkpointed before that rule.
|
|
52
|
+
if (params.length !== 2 || params[1] !== false || typeof params[0] !== 'string' || (!['latest', 'finalized', 'safe'].includes(params[0]) && !/^0x(?:0|[1-9a-fA-F][a-fA-F0-9]*)$/.test(params[0])))
|
|
53
|
+
throw new CheckpointIntegrityError();
|
|
54
|
+
if (receipt.snapshot && params[0] !== `0x${BigInt(receipt.snapshot.blockNumber).toString(16)}`)
|
|
55
|
+
throw new CheckpointIntegrityError();
|
|
56
|
+
}
|
|
57
|
+
else if (['eth_getCode', 'eth_getStorageAt', 'eth_call'].includes(method)) {
|
|
58
|
+
if (!receipt.snapshot || (receipt.finality !== 'sequencer-confirmed' && receipt.finality !== 'finalized'))
|
|
59
|
+
throw new CheckpointIntegrityError();
|
|
60
|
+
if (input.snapshotBlock !== undefined && receipt.snapshot.blockNumber !== input.snapshotBlock)
|
|
61
|
+
throw new CheckpointIntegrityError();
|
|
62
|
+
const target = method === 'eth_call' && typeof params[0] === 'object' && params[0] !== null && !Array.isArray(params[0])
|
|
63
|
+
? params[0].to : params[0];
|
|
64
|
+
// Token Researcher 0.3.0: a Multicall3 read is in scope only as this token's discovery lookup or a bounded balances/pool-state read.
|
|
65
|
+
const market = method === 'eth_call' && marketCallInScope(params[0], input.address, receipt.chainId);
|
|
66
|
+
if (((typeof target !== 'string' || target.toLowerCase() !== input.address.toLowerCase()) && !market)
|
|
67
|
+
|| !pinnedBlockParameter(params.at(-1), receipt.snapshot, input.chain)
|
|
68
|
+
|| params.length !== (method === 'eth_getStorageAt' ? 3 : 2))
|
|
69
|
+
throw new CheckpointIntegrityError();
|
|
70
|
+
}
|
|
71
|
+
else if (method === HOLDER_ACTIVITY_METHOD) {
|
|
72
|
+
// Token Researcher 0.3.0: exactly one newest-first transfer page of this token, at or before the pinned block.
|
|
73
|
+
if (!receipt.snapshot || receipt.finality !== 'sequencer-confirmed' || !marketTransferRequestInScope(params, input.address, receipt.snapshot.blockNumber))
|
|
74
|
+
throw new CheckpointIntegrityError();
|
|
75
|
+
}
|
|
76
|
+
else
|
|
77
|
+
throw new CheckpointIntegrityError();
|
|
78
|
+
return receipt;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error instanceof CheckpointIntegrityError)
|
|
82
|
+
throw error;
|
|
83
|
+
throw new CheckpointIntegrityError();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Transaction Inspector receipts may only contain reads derivable from the accepted payload (sender, target and decoded grantees). */
|
|
87
|
+
export function validateInspectionReceipt(evidence, input) {
|
|
88
|
+
try {
|
|
89
|
+
const receipt = InspectionEvidenceItemSchema.parse(evidence);
|
|
90
|
+
if (!chainIdentityMatches(receipt, input.chain) || !inspectionReceiptInScope(receipt, input))
|
|
91
|
+
throw new CheckpointIntegrityError();
|
|
92
|
+
return receipt;
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (error instanceof CheckpointIntegrityError)
|
|
96
|
+
throw error;
|
|
97
|
+
throw new CheckpointIntegrityError();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Watchtower receipts may only contain identity, window headers and the exact log filters derived from the accepted rules. */
|
|
101
|
+
export function validateWatchReceipt(evidence, input) {
|
|
102
|
+
try {
|
|
103
|
+
const receipt = WatchEvidenceItemSchema.parse(evidence);
|
|
104
|
+
if (!chainIdentityMatches(receipt, input.chain) || !watchReceiptInScope(receipt, input))
|
|
105
|
+
throw new CheckpointIntegrityError();
|
|
106
|
+
return receipt;
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (error instanceof CheckpointIntegrityError)
|
|
110
|
+
throw error;
|
|
111
|
+
throw new CheckpointIntegrityError();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** Solana inspector receipts: the declared tool, read-only allowlisted methods, the accepted cluster, and private simulation payloads. */
|
|
115
|
+
export function validateSolanaInspectionReceipt(evidence, input) {
|
|
116
|
+
try {
|
|
117
|
+
const receipt = ToolEvidenceItemSchema.parse(evidence);
|
|
118
|
+
if (receipt.tool !== SOLANA_INSPECTION_TOOL || receipt.capability !== 'solana.read' || !SOLANA_INSPECTION_RPC_METHODS.includes(receipt.source.method))
|
|
119
|
+
throw new CheckpointIntegrityError();
|
|
120
|
+
if (receipt.chain !== runCoverageChain(input) || contentHash(receipt.result) !== receipt.contentHash)
|
|
121
|
+
throw new CheckpointIntegrityError();
|
|
122
|
+
if (receipt.source.method === 'simulateTransaction' && receipt.privacy !== 'private-input')
|
|
123
|
+
throw new CheckpointIntegrityError();
|
|
124
|
+
return receipt;
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (error instanceof CheckpointIntegrityError)
|
|
128
|
+
throw error;
|
|
129
|
+
throw new CheckpointIntegrityError();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** Kind dispatch on the accepted input shape; the worker separately checks that the input kind matches the pinned manifest kind. */
|
|
133
|
+
export function validateRunReceipt(evidence, input, manifest) {
|
|
134
|
+
const kind = runInputKind(input);
|
|
135
|
+
if (kind === 'solana-transaction-inspection')
|
|
136
|
+
return validateSolanaInspectionReceipt(evidence, input);
|
|
137
|
+
// P02/P03 indexed agents: declared tools, the read-method allowlist and the accepted chain (tool envelopes, like workflows).
|
|
138
|
+
if (kind === 'deployer-investigation' || kind === 'wallet-analysis') {
|
|
139
|
+
try {
|
|
140
|
+
return kind === 'deployer-investigation' ? validateDeployerReceipt(evidence, input) : validateWalletReceipt(evidence, input);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
throw new CheckpointIntegrityError();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (kind === 'workflow') {
|
|
147
|
+
const tools = manifest?.tools;
|
|
148
|
+
const connectors = manifest?.connectors;
|
|
149
|
+
try {
|
|
150
|
+
return validateWorkflowReceipt(evidence, input, Array.isArray(tools) ? { tools: tools, ...(Array.isArray(connectors) ? { connectors: connectors } : {}) } : undefined);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
throw new CheckpointIntegrityError();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return kind === 'token-research' ? validateReceipt(evidence, input) : kind === 'watchtower' ? validateWatchReceipt(evidence, input) : validateInspectionReceipt(evidence, input);
|
|
157
|
+
}
|
|
158
|
+
export function restoreCheckpoints(run, rows) {
|
|
159
|
+
// Workflow and Solana receipts carry tool snapshots rather than one EVM chain id and block.
|
|
160
|
+
const inputHash = contentHash(run.input);
|
|
161
|
+
const evidence = [];
|
|
162
|
+
const workflow = ['workflow', 'solana-transaction-inspection', 'deployer-investigation', 'wallet-analysis'].includes(runInputKind(run.input));
|
|
163
|
+
const operations = new Set();
|
|
164
|
+
let snapshot = null;
|
|
165
|
+
const completedReads = new Map();
|
|
166
|
+
let identityChainId;
|
|
167
|
+
for (const row of rows) {
|
|
168
|
+
if (row.run_id !== run.id || row.version_id !== run.version_id || row.manifest_hash !== run.manifest_hash || row.input_hash !== inputHash
|
|
169
|
+
|| !Number.isSafeInteger(row.operation) || row.operation <= 0 || row.operation > run.rpc_used || operations.has(row.operation)
|
|
170
|
+
|| !Number.isSafeInteger(row.source_fence) || row.source_fence <= 0 || row.source_fence > run.fence || checkpointHash(row) !== row.checkpoint_hash)
|
|
171
|
+
throw new CheckpointIntegrityError();
|
|
172
|
+
operations.add(row.operation);
|
|
173
|
+
const validated = validateRunReceipt(row.evidence, run.input, run.manifest);
|
|
174
|
+
// Workflow receipts come from several tools, each with its own snapshot; reports show differing snapshots explicitly.
|
|
175
|
+
if (workflow) {
|
|
176
|
+
evidence.push(validated);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const receipt = validated;
|
|
180
|
+
if (identityChainId !== undefined && receipt.chainId !== identityChainId)
|
|
181
|
+
throw new CheckpointIntegrityError();
|
|
182
|
+
identityChainId = receipt.chainId;
|
|
183
|
+
if (receipt.snapshot) {
|
|
184
|
+
if (snapshot && (snapshot.blockNumber !== receipt.snapshot.blockNumber || snapshot.blockHash.toLowerCase() !== receipt.snapshot.blockHash.toLowerCase()))
|
|
185
|
+
throw new CheckpointIntegrityError();
|
|
186
|
+
snapshot = receipt.snapshot;
|
|
187
|
+
if (STATE_METHODS.includes(receipt.source.method)
|
|
188
|
+
&& (receipt.completeness === 'complete-for-request' || receipt.contentHash === contentHash({ error: 'contract-reverted' }))) {
|
|
189
|
+
const key = contentHash({ method: receipt.source.method, request: receipt.request, snapshot: { ...receipt.snapshot, blockHash: receipt.snapshot.blockHash.toLowerCase() } });
|
|
190
|
+
const previous = completedReads.get(key);
|
|
191
|
+
if (previous && previous !== receipt.contentHash)
|
|
192
|
+
throw new CheckpointIntegrityError();
|
|
193
|
+
completedReads.set(key, receipt.contentHash);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
evidence.push(receipt);
|
|
197
|
+
}
|
|
198
|
+
if (snapshot) {
|
|
199
|
+
const identity = evidence.some((receipt) => receipt.source.method === 'eth_chainId' && receipt.completeness === 'complete-for-request'
|
|
200
|
+
&& typeof receipt.result === 'string' && /^0x(?:0|[1-9a-fA-F][a-fA-F0-9]*)$/.test(receipt.result)
|
|
201
|
+
&& BigInt(receipt.result) === BigInt(identityChainId));
|
|
202
|
+
if (!identity)
|
|
203
|
+
throw new CheckpointIntegrityError();
|
|
204
|
+
// A data read can only be reused when its selected block was actually captured during snapshot selection.
|
|
205
|
+
const header = evidence.some((receipt) => receipt.source.method === 'eth_getBlockByNumber' && receipt.completeness === 'complete-for-request'
|
|
206
|
+
&& receipt.snapshot === null && typeof receipt.result === 'object' && receipt.result !== null && !Array.isArray(receipt.result)
|
|
207
|
+
&& typeof receipt.result.hash === 'string' && receipt.result.hash.toLowerCase() === snapshot.blockHash.toLowerCase() && typeof receipt.result.number === 'string'
|
|
208
|
+
&& /^0x(?:0|[1-9a-fA-F][a-fA-F0-9]*)$/.test(receipt.result.number)
|
|
209
|
+
&& BigInt(receipt.result.number).toString() === snapshot.blockNumber);
|
|
210
|
+
if (!header)
|
|
211
|
+
throw new CheckpointIntegrityError();
|
|
212
|
+
}
|
|
213
|
+
return { manifestHash: run.manifest_hash, inputHash, evidence };
|
|
214
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type ToolEvidenceItem, type WorkflowManifest, type WorkflowRunInput, type WorkflowToolDeclaration } from '../../contracts/src/workflow.js';
|
|
2
|
+
/**
|
|
3
|
+
* Runtime permission policy for `agentex.workflow.v1` (D04). It sits between workflow nodes and the tool/transport boundary and
|
|
4
|
+
* consults only typed manifest grants: declared tool and version, capability method allowlist, chain, address/domain scope,
|
|
5
|
+
* per-tool and run budgets, and the report-only destination. Names, descriptions and listing text are never read.
|
|
6
|
+
*/
|
|
7
|
+
export declare class WorkflowPermissionError extends Error {
|
|
8
|
+
readonly code = "WORKFLOW_PERMISSION_DENIED";
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
11
|
+
/** The only tracer a workflow may request: net balance and nonce changes, no storage or code, so a trace stays small and bounded. */
|
|
12
|
+
export declare const PRESTATE_DIFF_TRACER_CONFIG: Readonly<{
|
|
13
|
+
tracer: "prestateTracer";
|
|
14
|
+
tracerConfig: Readonly<{
|
|
15
|
+
diffMode: true;
|
|
16
|
+
disableStorage: true;
|
|
17
|
+
disableCode: true;
|
|
18
|
+
}>;
|
|
19
|
+
}>;
|
|
20
|
+
/** Multicall3 (the same address on every supported EVM chain). A batched read targets Multicall3 and every contract inside the batch. */
|
|
21
|
+
export declare const MULTICALL3 = "0xca11bde05977b3631167028862be2a173976ca11";
|
|
22
|
+
/** The accepted input's address-typed values. These are the only initial address grants. */
|
|
23
|
+
export declare function workflowAddressScope(manifest: Pick<WorkflowManifest, 'inputSchema'>, input: WorkflowRunInput): Set<string>;
|
|
24
|
+
export interface WorkflowPermit {
|
|
25
|
+
/** Resolves the grant for a declared tool id; throws for undeclared tools or chains. */
|
|
26
|
+
grant(toolId: string): WorkflowToolDeclaration;
|
|
27
|
+
/** Before the run-wide runtime permit: method allowlist and the per-tool call ceiling. */
|
|
28
|
+
beforeCall(toolId: string, method: string): void;
|
|
29
|
+
/** Called by the guarded transport: consumes a prior permit for this method and checks address/domain scope. */
|
|
30
|
+
checkRequest(toolId: string, method: string, params: readonly unknown[]): void;
|
|
31
|
+
observe(toolId: string, method: string, outcome: {
|
|
32
|
+
result: unknown;
|
|
33
|
+
} | {
|
|
34
|
+
error: true;
|
|
35
|
+
}): void;
|
|
36
|
+
/** Accepts evidence only if it matches a response the guarded transport actually returned to this tool. */
|
|
37
|
+
admitEvidence(toolId: string, raw: unknown): ToolEvidenceItem;
|
|
38
|
+
/** Adds addresses from validated tool output when the manifest grants `input-derived` scope. */
|
|
39
|
+
derive(value: unknown): void;
|
|
40
|
+
calls(toolId: string): number;
|
|
41
|
+
scope(): string[];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Per-tool platform contracts. A registry tool may declare fixed, reviewed contract addresses it reads on a chain (for example DEX factories,
|
|
45
|
+
* a lending pool or Multicall3). They are in scope for that tool only; buyer inputs, tool output and descriptive text never add to them.
|
|
46
|
+
*/
|
|
47
|
+
export interface WorkflowPermitOptions {
|
|
48
|
+
curatedAddresses?: (toolId: string) => ReadonlySet<string>;
|
|
49
|
+
}
|
|
50
|
+
export declare function createWorkflowPermit(manifest: WorkflowManifest, input: WorkflowRunInput, options?: WorkflowPermitOptions): WorkflowPermit;
|
|
51
|
+
/** Receipt check used by the durable run store before a workflow checkpoint is saved or restored. */
|
|
52
|
+
export declare function validateWorkflowReceipt(evidence: unknown, input: WorkflowRunInput, manifest?: {
|
|
53
|
+
tools: readonly WorkflowToolDeclaration[];
|
|
54
|
+
connectors?: readonly {
|
|
55
|
+
id: string;
|
|
56
|
+
}[];
|
|
57
|
+
}): ToolEvidenceItem;
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { decodeFunctionData, parseAbi } from 'viem';
|
|
2
|
+
import { CONNECTOR_CALL_METHOD, connectorToolRef } from '../../contracts/src/workflow.js';
|
|
3
|
+
import { contentHash } from '../../contracts/src/index.js';
|
|
4
|
+
import { CAPABILITY_METHODS, ToolEvidenceItemSchema } from '../../contracts/src/workflow.js';
|
|
5
|
+
/**
|
|
6
|
+
* Runtime permission policy for `agentex.workflow.v1` (D04). It sits between workflow nodes and the tool/transport boundary and
|
|
7
|
+
* consults only typed manifest grants: declared tool and version, capability method allowlist, chain, address/domain scope,
|
|
8
|
+
* per-tool and run budgets, and the report-only destination. Names, descriptions and listing text are never read.
|
|
9
|
+
*/
|
|
10
|
+
export class WorkflowPermissionError extends Error {
|
|
11
|
+
code = 'WORKFLOW_PERMISSION_DENIED';
|
|
12
|
+
constructor(message) { super(message); this.name = 'WorkflowPermissionError'; }
|
|
13
|
+
}
|
|
14
|
+
const EVM_ADDRESS = /^0x[0-9a-fA-F]{40}$/;
|
|
15
|
+
const ABI_ADDRESS_WORD = /^0x0{24}([0-9a-fA-F]{40})$/;
|
|
16
|
+
const SOLANA_ADDRESS = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
17
|
+
const SOLANA_ADDRESS_METHODS = new Set(['getAccountInfo', 'getBalance', 'getSignaturesForAddress', 'getTokenSupply', 'getTokenLargestAccounts', 'getMultipleAccounts']);
|
|
18
|
+
const ZERO_ADDRESS = `0x${'0'.repeat(40)}`;
|
|
19
|
+
const MISSING_ADDRESS = Symbol('missing address');
|
|
20
|
+
/** Requests that return one page of an unbounded sequence. Each counts against `limits.maximumPages`. */
|
|
21
|
+
const PAGED_METHODS = new Set(['eth_getLogs', 'getSignaturesForAddress']);
|
|
22
|
+
/** The only tracer a workflow may request: net balance and nonce changes, no storage or code, so a trace stays small and bounded. */
|
|
23
|
+
export const PRESTATE_DIFF_TRACER_CONFIG = Object.freeze({ tracer: 'prestateTracer', tracerConfig: Object.freeze({ diffMode: true, disableStorage: true, disableCode: true }) });
|
|
24
|
+
const TRANSACTION_HASH = /^0x[0-9a-fA-F]{64}$/;
|
|
25
|
+
const blockNumberOf = (value) => typeof value === 'string' && /^0x(?:0|[1-9a-fA-F][0-9a-fA-F]{0,15})$/.test(value) ? BigInt(value) : null;
|
|
26
|
+
const CHAIN_IDS = { ethereum: [1], base: [8453], robinhood: [4663, 46630] };
|
|
27
|
+
const normalize = (value) => value === undefined ? null : JSON.parse(JSON.stringify(value, (_key, item) => typeof item === 'bigint' ? item.toString() : item));
|
|
28
|
+
const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
29
|
+
function collectStrings(value, out, depth = 0) {
|
|
30
|
+
if (out.length > 256 || depth > 6)
|
|
31
|
+
return;
|
|
32
|
+
if (typeof value === 'string')
|
|
33
|
+
out.push(value);
|
|
34
|
+
else if (Array.isArray(value))
|
|
35
|
+
for (const item of value)
|
|
36
|
+
collectStrings(item, out, depth + 1);
|
|
37
|
+
else if (value !== null && typeof value === 'object')
|
|
38
|
+
for (const item of Object.values(value))
|
|
39
|
+
collectStrings(item, out, depth + 1);
|
|
40
|
+
}
|
|
41
|
+
/** Addresses in arbitrary data: plain 20-byte addresses and ABI-encoded address words. Used only to widen `input-derived` scope. */
|
|
42
|
+
function derivedEvmAddresses(value) {
|
|
43
|
+
const strings = [];
|
|
44
|
+
collectStrings(value, strings);
|
|
45
|
+
return strings.flatMap((item) => EVM_ADDRESS.test(item) ? [item.toLowerCase()] : ABI_ADDRESS_WORD.test(item) ? [`0x${ABI_ADDRESS_WORD.exec(item)[1].toLowerCase()}`] : []);
|
|
46
|
+
}
|
|
47
|
+
/** Multicall3 (the same address on every supported EVM chain). A batched read targets Multicall3 and every contract inside the batch. */
|
|
48
|
+
export const MULTICALL3 = '0xca11bde05977b3631167028862be2a173976ca11';
|
|
49
|
+
const AGGREGATE3 = parseAbi(['struct Call3 { address target; bool allowFailure; bytes callData; }', 'function aggregate3(Call3[] calls) payable returns ((bool success, bytes returnData)[] returnData)']);
|
|
50
|
+
/** Every target of an aggregate3 batch, or a missing marker when the payload is not a plain aggregate3 call (other Multicall3 entry points are refused). */
|
|
51
|
+
function multicallTargets(data) {
|
|
52
|
+
if (typeof data !== 'string' || !/^0x(?:[0-9a-fA-F]{2})*$/.test(data) || data.length > 2 + 2 * 131072)
|
|
53
|
+
return [MISSING_ADDRESS];
|
|
54
|
+
try {
|
|
55
|
+
const decoded = decodeFunctionData({ abi: AGGREGATE3, data: data });
|
|
56
|
+
const calls = decoded.args[0];
|
|
57
|
+
return calls.length > 0 && calls.length <= 256 ? calls.map((call) => call.target) : [MISSING_ADDRESS];
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return [MISSING_ADDRESS];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Accounts an EVM read method actually targets. Unknown shapes are refused rather than ignored. */
|
|
64
|
+
function evmTargets(method, params) {
|
|
65
|
+
const first = params[0];
|
|
66
|
+
const object = record(first);
|
|
67
|
+
switch (method) {
|
|
68
|
+
case 'eth_getCode':
|
|
69
|
+
case 'eth_getBalance':
|
|
70
|
+
case 'eth_getTransactionCount':
|
|
71
|
+
case 'eth_getStorageAt': return [first];
|
|
72
|
+
case 'eth_call':
|
|
73
|
+
case 'eth_estimateGas': {
|
|
74
|
+
if (!object)
|
|
75
|
+
return [first];
|
|
76
|
+
const direct = [object.to, ...(object.from === undefined ? [] : [object.from])];
|
|
77
|
+
return typeof object.to === 'string' && object.to.toLowerCase() === MULTICALL3 ? [...direct, ...multicallTargets(object.data ?? object.input)] : direct;
|
|
78
|
+
}
|
|
79
|
+
case 'eth_getLogs': {
|
|
80
|
+
if (!object)
|
|
81
|
+
return [first];
|
|
82
|
+
const topics = [];
|
|
83
|
+
collectStrings(object.topics ?? [], topics);
|
|
84
|
+
// A filter without an address matches every contract on the chain; that is never inside an address scope, so it is refused.
|
|
85
|
+
const addresses = [object.address ?? []].flat();
|
|
86
|
+
return [...(addresses.length ? addresses : [MISSING_ADDRESS]), ...topics.filter((topic) => ABI_ADDRESS_WORD.test(topic)).map((topic) => `0x${ABI_ADDRESS_WORD.exec(topic)[1]}`)];
|
|
87
|
+
}
|
|
88
|
+
default: return [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** The accepted input's address-typed values. These are the only initial address grants. */
|
|
92
|
+
export function workflowAddressScope(manifest, input) {
|
|
93
|
+
const scope = new Set();
|
|
94
|
+
for (const [key, field] of Object.entries(manifest.inputSchema.properties)) {
|
|
95
|
+
const value = input.values[key];
|
|
96
|
+
if (typeof value !== 'string')
|
|
97
|
+
continue;
|
|
98
|
+
if (field.type === 'evm-address' && EVM_ADDRESS.test(value))
|
|
99
|
+
scope.add(value.toLowerCase());
|
|
100
|
+
if (field.type === 'solana-address' && SOLANA_ADDRESS.test(value))
|
|
101
|
+
scope.add(value);
|
|
102
|
+
}
|
|
103
|
+
return scope;
|
|
104
|
+
}
|
|
105
|
+
export function createWorkflowPermit(manifest, input, options = {}) {
|
|
106
|
+
const tools = new Map(manifest.tools.map((tool) => [tool.id, tool]));
|
|
107
|
+
const scope = workflowAddressScope(manifest, input);
|
|
108
|
+
const calls = new Map();
|
|
109
|
+
const pending = new Map();
|
|
110
|
+
const observed = new Map();
|
|
111
|
+
// D04 data-range and page limits: the head block is learned only from a permitted `eth_getBlockByNumber('latest')` response. No finality tag is read.
|
|
112
|
+
let headBlock = null;
|
|
113
|
+
let awaitingHead = false;
|
|
114
|
+
let pages = 0;
|
|
115
|
+
const deny = (message) => { throw new WorkflowPermissionError(message); };
|
|
116
|
+
const grant = (toolId) => {
|
|
117
|
+
const tool = tools.get(toolId) ?? deny(`Tool ${toolId} is not declared by this immutable version.`);
|
|
118
|
+
if (!manifest.chains.includes(input.chain) || !tool.chains.includes(input.chain))
|
|
119
|
+
deny(`Tool ${toolId} is not granted on ${input.chain}.`);
|
|
120
|
+
return tool;
|
|
121
|
+
};
|
|
122
|
+
const inScope = (address, toolId) => typeof address === 'string' && EVM_ADDRESS.test(address) && (address.toLowerCase() === ZERO_ADDRESS || scope.has(address.toLowerCase())
|
|
123
|
+
|| (toolId !== undefined && (options.curatedAddresses?.(toolId).has(address.toLowerCase()) ?? false)));
|
|
124
|
+
const curatedSolana = (address, toolId) => typeof address === 'string' && (options.curatedAddresses?.(toolId).has(address) ?? false);
|
|
125
|
+
const derive = (value) => {
|
|
126
|
+
if (manifest.permissions.addressScope !== 'input-derived')
|
|
127
|
+
return;
|
|
128
|
+
for (const address of derivedEvmAddresses(value))
|
|
129
|
+
scope.add(address);
|
|
130
|
+
if (input.chain === 'solana') {
|
|
131
|
+
const strings = [];
|
|
132
|
+
collectStrings(value, strings);
|
|
133
|
+
for (const item of strings)
|
|
134
|
+
if (SOLANA_ADDRESS.test(item))
|
|
135
|
+
scope.add(item);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
return {
|
|
139
|
+
grant,
|
|
140
|
+
beforeCall(toolId, method) {
|
|
141
|
+
const tool = grant(toolId);
|
|
142
|
+
if (!CAPABILITY_METHODS[tool.capability].includes(method))
|
|
143
|
+
deny(`${method} is outside the ${tool.capability} capability of ${toolId}.`);
|
|
144
|
+
const used = calls.get(toolId) ?? 0;
|
|
145
|
+
if (used >= tool.maximumCalls)
|
|
146
|
+
deny(`Tool ${toolId} reached its declared ceiling of ${tool.maximumCalls} calls.`);
|
|
147
|
+
calls.set(toolId, used + 1);
|
|
148
|
+
pending.set(`${toolId}\n${method}`, (pending.get(`${toolId}\n${method}`) ?? 0) + 1);
|
|
149
|
+
},
|
|
150
|
+
checkRequest(toolId, method, params) {
|
|
151
|
+
const tool = grant(toolId);
|
|
152
|
+
const key = `${toolId}\n${method}`;
|
|
153
|
+
const permits = pending.get(key) ?? 0;
|
|
154
|
+
if (permits < 1)
|
|
155
|
+
deny(`${toolId} attempted ${method} without a runtime permit.`);
|
|
156
|
+
pending.set(key, permits - 1);
|
|
157
|
+
if (!Array.isArray(params) || params.length > 16)
|
|
158
|
+
deny('Request parameters are malformed.');
|
|
159
|
+
if (PAGED_METHODS.has(method)) {
|
|
160
|
+
if (pages >= manifest.limits.maximumPages)
|
|
161
|
+
deny(`${toolId} requested page ${pages + 1} with ${method}; the version allows at most ${manifest.limits.maximumPages} paged request${manifest.limits.maximumPages === 1 ? '' : 's'} per run.`);
|
|
162
|
+
pages++;
|
|
163
|
+
}
|
|
164
|
+
if (tool.capability === 'evm.read') {
|
|
165
|
+
for (const target of evmTargets(method, params))
|
|
166
|
+
if (!inScope(target, toolId))
|
|
167
|
+
deny(target === MISSING_ADDRESS ? `${toolId} requested ${method} without an address; unscoped log filters are outside the ${manifest.permissions.addressScope} address scope.` : `${toolId} requested ${typeof target === 'string' ? target : 'a malformed target'}, outside the ${manifest.permissions.addressScope} address scope.`);
|
|
168
|
+
if (method === 'eth_getBlockByNumber')
|
|
169
|
+
awaitingHead = params[0] === 'latest';
|
|
170
|
+
if (method === 'debug_traceTransaction') {
|
|
171
|
+
const config = record(params[1]);
|
|
172
|
+
const tracer = record(config?.tracerConfig);
|
|
173
|
+
if (params.length !== 2 || typeof params[0] !== 'string' || !TRANSACTION_HASH.test(params[0]) || !config || Object.keys(config).length !== 2 || config.tracer !== PRESTATE_DIFF_TRACER_CONFIG.tracer
|
|
174
|
+
|| !tracer || Object.keys(tracer).length !== 3 || tracer.diffMode !== true || tracer.disableStorage !== true || tracer.disableCode !== true)
|
|
175
|
+
deny(`${toolId} requested debug_traceTransaction without the fixed prestate-diff tracer; other tracers are not permitted.`);
|
|
176
|
+
}
|
|
177
|
+
if (method === 'eth_getLogs') {
|
|
178
|
+
const filter = record(params[0]);
|
|
179
|
+
const width = 'blockHash' in filter ? 1n : (() => {
|
|
180
|
+
const resolveTag = (tag) => tag === 'latest' ? headBlock : blockNumberOf(tag);
|
|
181
|
+
const from = resolveTag(filter.fromBlock);
|
|
182
|
+
const to = resolveTag(filter.toBlock);
|
|
183
|
+
if (from === null || to === null)
|
|
184
|
+
deny(`${toolId} requested eth_getLogs from ${String(filter.fromBlock ?? 'an omitted block')} to ${String(filter.toBlock ?? 'an omitted block')}; log ranges need numeric bounds (or 'latest' after the latest head was read).`);
|
|
185
|
+
if (to < from)
|
|
186
|
+
deny(`${toolId} requested a descending eth_getLogs range.`);
|
|
187
|
+
return to - from + 1n;
|
|
188
|
+
})();
|
|
189
|
+
if (width > BigInt(manifest.limits.maximumBlockRange))
|
|
190
|
+
deny(`${toolId} requested eth_getLogs over ${width} block${width === 1n ? '' : 's'}; the version allows at most ${manifest.limits.maximumBlockRange}.`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
else if (tool.capability === 'solana.read') {
|
|
194
|
+
if (method === 'simulateTransaction')
|
|
195
|
+
deny(`${toolId} requested simulateTransaction; the accounts of a proposed payload cannot be bound to the ${manifest.permissions.addressScope} scope, so workflows cannot simulate.`);
|
|
196
|
+
if (SOLANA_ADDRESS_METHODS.has(method))
|
|
197
|
+
for (const address of [params[0]].flat())
|
|
198
|
+
if (typeof address !== 'string' || !(scope.has(address) || curatedSolana(address, toolId)))
|
|
199
|
+
deny(`${toolId} requested ${method} for ${typeof address === 'string' ? 'a Solana account' : 'no account'} outside the ${manifest.permissions.addressScope} scope.`);
|
|
200
|
+
// Validator reads are filtered to one in-scope account: an unfiltered vote-account or block-production list is refused (it is also megabytes).
|
|
201
|
+
if (method === 'getVoteAccounts' || method === 'getBlockProduction') {
|
|
202
|
+
const config = record(params[0]);
|
|
203
|
+
const key = method === 'getVoteAccounts' ? 'votePubkey' : 'identity';
|
|
204
|
+
const account = config?.[key];
|
|
205
|
+
if (params.length > 1 || typeof account !== 'string' || !(scope.has(account) || curatedSolana(account, toolId)))
|
|
206
|
+
deny(`${toolId} requested ${method} without an in-scope ${key}; unfiltered validator lists are refused.`);
|
|
207
|
+
}
|
|
208
|
+
if (method === 'getRecentPerformanceSamples' && (params.length !== 1 || typeof params[0] !== 'number' || !Number.isInteger(params[0]) || params[0] < 1 || params[0] > 60))
|
|
209
|
+
deny(`${toolId} requested getRecentPerformanceSamples without a limit of 1 to 60.`);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
let url = null;
|
|
213
|
+
try {
|
|
214
|
+
url = new URL(String(params[0]));
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
deny('HTTP reads need an absolute URL.');
|
|
218
|
+
}
|
|
219
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.port || !manifest.permissions.domains.includes(url.hostname))
|
|
220
|
+
deny(`${url.hostname || 'This URL'} is not an allowed https domain for ${toolId}.`);
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
observe(toolId, method, outcome) {
|
|
224
|
+
const key = `${toolId}\n${method}`;
|
|
225
|
+
const entry = observed.get(key) ?? { hashes: [], attempts: 0 };
|
|
226
|
+
entry.attempts++;
|
|
227
|
+
if ('result' in outcome)
|
|
228
|
+
entry.hashes.push(contentHash(normalize(outcome.result)));
|
|
229
|
+
observed.set(key, entry);
|
|
230
|
+
if (method === 'eth_getBlockByNumber' && awaitingHead && 'result' in outcome) {
|
|
231
|
+
const number = blockNumberOf(record(outcome.result)?.number);
|
|
232
|
+
if (number !== null && (headBlock === null || number > headBlock))
|
|
233
|
+
headBlock = number;
|
|
234
|
+
awaitingHead = false;
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
admitEvidence(toolId, raw) {
|
|
238
|
+
const tool = grant(toolId);
|
|
239
|
+
const parsed = ToolEvidenceItemSchema.safeParse(raw);
|
|
240
|
+
if (!parsed.success)
|
|
241
|
+
deny(`${toolId} emitted evidence outside the shared evidence envelope.`);
|
|
242
|
+
const evidence = parsed.data;
|
|
243
|
+
if (evidence.tool !== `${tool.id}@${tool.version}` || evidence.capability !== tool.capability)
|
|
244
|
+
deny(`Evidence names ${evidence.tool} (${evidence.capability}), not the declared ${tool.id}@${tool.version}.`);
|
|
245
|
+
if (evidence.chain !== input.chain)
|
|
246
|
+
deny('Evidence chain differs from the accepted input chain.');
|
|
247
|
+
if (evidence.snapshot?.kind === 'evm-block' && !(CHAIN_IDS[input.chain] ?? []).includes(evidence.snapshot.chainId))
|
|
248
|
+
deny('Evidence block identity does not match the accepted chain.');
|
|
249
|
+
if (evidence.source.kind === 'offline') {
|
|
250
|
+
if (evidence.trust !== 'offline-decoding')
|
|
251
|
+
deny('Offline evidence can only describe offline decoding.');
|
|
252
|
+
return evidence;
|
|
253
|
+
}
|
|
254
|
+
if (!CAPABILITY_METHODS[tool.capability].includes(evidence.source.method))
|
|
255
|
+
deny(`Evidence method ${evidence.source.method} is outside ${tool.capability}.`);
|
|
256
|
+
const entry = observed.get(`${toolId}\n${evidence.source.method}`);
|
|
257
|
+
if (!entry || entry.attempts < 1)
|
|
258
|
+
deny(`${toolId} emitted evidence for ${evidence.source.method}, which it never requested through the guarded transport.`);
|
|
259
|
+
if (evidence.completeness === 'complete-for-request' || evidence.completeness === 'empty-result') {
|
|
260
|
+
const position = entry.hashes.indexOf(evidence.contentHash);
|
|
261
|
+
if (position < 0)
|
|
262
|
+
deny(`${toolId} emitted a result the guarded transport never returned (fabricated evidence).`);
|
|
263
|
+
entry.hashes.splice(position, 1);
|
|
264
|
+
derive(evidence.result);
|
|
265
|
+
}
|
|
266
|
+
entry.attempts--;
|
|
267
|
+
return evidence;
|
|
268
|
+
},
|
|
269
|
+
derive,
|
|
270
|
+
calls: (toolId) => calls.get(toolId) ?? 0,
|
|
271
|
+
scope: () => [...scope],
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
/** Receipt check used by the durable run store before a workflow checkpoint is saved or restored. */
|
|
275
|
+
export function validateWorkflowReceipt(evidence, input, manifest) {
|
|
276
|
+
const receipt = ToolEvidenceItemSchema.parse(evidence);
|
|
277
|
+
if (receipt.chain !== input.chain)
|
|
278
|
+
throw new WorkflowPermissionError('Receipt chain does not match the run contract.');
|
|
279
|
+
if (receipt.snapshot?.kind === 'evm-block' && !(CHAIN_IDS[input.chain] ?? []).includes(receipt.snapshot.chainId))
|
|
280
|
+
throw new WorkflowPermissionError('Receipt network identity does not match the chain.');
|
|
281
|
+
const methodAllowed = (capability) => receipt.source.kind === 'offline' || CAPABILITY_METHODS[capability].includes(receipt.source.method);
|
|
282
|
+
if (manifest) {
|
|
283
|
+
const tool = manifest.tools.find((item) => `${item.id}@${item.version}` === receipt.tool);
|
|
284
|
+
// N03: a pinned remote connector's receipt is one metered POST envelope of untrusted third-party content.
|
|
285
|
+
const connector = tool ? undefined : manifest.connectors?.find((item) => connectorToolRef(item.id) === receipt.tool);
|
|
286
|
+
if (connector) {
|
|
287
|
+
if (receipt.capability !== 'http.read' || receipt.source.kind !== 'http' || receipt.source.method !== CONNECTOR_CALL_METHOD)
|
|
288
|
+
throw new WorkflowPermissionError('Receipt is not a metered connector call envelope.');
|
|
289
|
+
}
|
|
290
|
+
else if (!tool || tool.capability !== receipt.capability || !methodAllowed(tool.capability))
|
|
291
|
+
throw new WorkflowPermissionError('Receipt tool or method is not granted by the pinned version.');
|
|
292
|
+
}
|
|
293
|
+
else if (!methodAllowed(receipt.capability))
|
|
294
|
+
throw new WorkflowPermissionError('Receipt method is not a read method.');
|
|
295
|
+
return receipt;
|
|
296
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runBuyerCli } from '../buyer-cli.js';
|
|
3
|
+
// Buyer commands use a scoped AGENTEX_API_KEY (never printed) against https://agentex.sh, or AGENTEX_API_ORIGIN / --api.
|
|
4
|
+
process.exitCode = await runBuyerCli(process.argv.slice(2), { env: process.env, stdout: text => process.stdout.write(text), stderr: text => process.stderr.write(text) });
|