otto-intel-mcp 0.1.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/LICENSE +21 -0
- package/README.md +102 -0
- package/dist/adapter/cdp-signer.d.ts +133 -0
- package/dist/adapter/cdp-signer.js +356 -0
- package/dist/adapter/cdp-signer.js.map +1 -0
- package/dist/adapter/chain.d.ts +36 -0
- package/dist/adapter/chain.js +65 -0
- package/dist/adapter/chain.js.map +1 -0
- package/dist/adapter/erc20.d.ts +39 -0
- package/dist/adapter/erc20.js +17 -0
- package/dist/adapter/erc20.js.map +1 -0
- package/dist/adapter/lifi-decode.d.ts +52 -0
- package/dist/adapter/lifi-decode.js +149 -0
- package/dist/adapter/lifi-decode.js.map +1 -0
- package/dist/adapter/refusal.d.ts +21 -0
- package/dist/adapter/refusal.js +55 -0
- package/dist/adapter/refusal.js.map +1 -0
- package/dist/adapter/sent-step.d.ts +15 -0
- package/dist/adapter/sent-step.js +6 -0
- package/dist/adapter/sent-step.js.map +1 -0
- package/dist/adapter/verify.d.ts +149 -0
- package/dist/adapter/verify.js +432 -0
- package/dist/adapter/verify.js.map +1 -0
- package/dist/adapter-cdp-index.d.ts +6 -0
- package/dist/adapter-cdp-index.js +7 -0
- package/dist/adapter-index.d.ts +15 -0
- package/dist/adapter-index.js +15 -0
- package/dist/artifact-id.d.ts +16 -0
- package/dist/artifact-id.js +60 -0
- package/dist/boot-redaction.d.ts +16 -0
- package/dist/boot-redaction.js +54 -0
- package/dist/data-source.d.ts +7 -0
- package/dist/data-source.js +2 -0
- package/dist/errors.d.ts +70 -0
- package/dist/errors.js +221 -0
- package/dist/execution-config.d.ts +232 -0
- package/dist/execution-config.js +443 -0
- package/dist/execution-delegated-definition.d.ts +165 -0
- package/dist/execution-delegated-definition.js +116 -0
- package/dist/execution-delegation-admin-definition.d.ts +208 -0
- package/dist/execution-delegation-admin-definition.js +170 -0
- package/dist/execution-delegation-admin.d.ts +74 -0
- package/dist/execution-delegation-admin.js +290 -0
- package/dist/execution-delegation-policy.d.ts +257 -0
- package/dist/execution-delegation-policy.js +279 -0
- package/dist/execution-delegation.d.ts +190 -0
- package/dist/execution-delegation.js +545 -0
- package/dist/execution-errors.d.ts +9 -0
- package/dist/execution-errors.js +128 -0
- package/dist/execution-index.d.ts +17 -0
- package/dist/execution-index.js +16 -0
- package/dist/execution-intent.d.ts +14 -0
- package/dist/execution-intent.js +36 -0
- package/dist/execution-registration.d.ts +113 -0
- package/dist/execution-registration.js +161 -0
- package/dist/execution-tool-definitions.d.ts +1103 -0
- package/dist/execution-tool-definitions.js +1051 -0
- package/dist/execution-tools.d.ts +85 -0
- package/dist/execution-tools.js +690 -0
- package/dist/execution-types.d.ts +274 -0
- package/dist/execution-types.js +157 -0
- package/dist/hyperliquid-info-client.d.ts +31 -0
- package/dist/hyperliquid-info-client.js +121 -0
- package/dist/hyperliquid-order-assertions.d.ts +89 -0
- package/dist/hyperliquid-order-assertions.js +325 -0
- package/dist/hyperliquid-order-builder.d.ts +47 -0
- package/dist/hyperliquid-order-builder.js +55 -0
- package/dist/lifi-bridge-assertions.d.ts +49 -0
- package/dist/lifi-bridge-assertions.js +303 -0
- package/dist/lifi-execution-client.d.ts +74 -0
- package/dist/lifi-execution-client.js +165 -0
- package/dist/lifi-fee-assertions.d.ts +32 -0
- package/dist/lifi-fee-assertions.js +103 -0
- package/dist/lifi-swap-assertions.d.ts +32 -0
- package/dist/lifi-swap-assertions.js +214 -0
- package/dist/local-config.d.ts +9 -0
- package/dist/local-config.js +45 -0
- package/dist/mcp-server.d.ts +4 -0
- package/dist/mcp-server.js +44 -0
- package/dist/polymarket-clob-client.d.ts +73 -0
- package/dist/polymarket-clob-client.js +186 -0
- package/dist/polymarket-order-assertions.d.ts +60 -0
- package/dist/polymarket-order-assertions.js +273 -0
- package/dist/polymarket-order-builder.d.ts +43 -0
- package/dist/polymarket-order-builder.js +74 -0
- package/dist/prepared-artifacts.d.ts +45 -0
- package/dist/prepared-artifacts.js +92 -0
- package/dist/stdio-main.d.ts +2 -0
- package/dist/stdio-main.js +20 -0
- package/dist/tool-definitions.d.ts +20 -0
- package/dist/tool-definitions.js +162 -0
- package/dist/x402-read-source.d.ts +11 -0
- package/dist/x402-read-source.js +77 -0
- package/package.json +162 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* execution-delegation-policy.ts — the product's CDP project policy that fences the delegated leg
|
|
3
|
+
* (Model B, browser plane; seat ruling D9 = α+, 2026-08-29).
|
|
4
|
+
*
|
|
5
|
+
* WHAT THE ENGINE FENCES, AND WHAT IT CANNOT. Coinbase's policy engine offers END-USER accounts exactly
|
|
6
|
+
* one fence: the project-scope policy ("Only one project-level policy can exist at any time"; account-
|
|
7
|
+
* scoped policies attach only to developer-owned accounts, and no rule can refer to "the sender's own
|
|
8
|
+
* address"). So this policy is a SHAPE fence with constant caps, shared by every delegated user:
|
|
9
|
+
* - approve: only on an allow-listed input token, only to the reviewed LI.FI diamond, only up to the
|
|
10
|
+
* per-swap cap (5,000 USD, Founder word 2026-08-29) in that token's atomic units — a bound on ONE
|
|
11
|
+
* swap, not a daily or total limit (zero stays allowed for the allowance clear);
|
|
12
|
+
* - call: only the LI.FI diamond, zero native value, only the two GenericSwapV3 ERC20→ERC20 entry points.
|
|
13
|
+
* The approve cap is what bounds the swap's notional on-chain: the diamond can only pull what is
|
|
14
|
+
* approved. Everything else — that `_receiver` is the end user, that `_minAmount` is at least the
|
|
15
|
+
* caller's floor, that the fee leg is the ruled attribution — is Otto's SERVER-SIDE 13-check verifier on
|
|
16
|
+
* every call (`adapter/verify.ts`), run with the end user's address as the server reads it from CDP.
|
|
17
|
+
*
|
|
18
|
+
* THE RESIDUAL, STATED PLAINLY (also in the PR body and in the consent copy the user reads):
|
|
19
|
+
* Coinbase's engine limits Otto to swaps through LI.FI up to the per-swap cap; Otto's own server checks
|
|
20
|
+
* where the output goes; the user can revoke at any time. The engine does NOT bind the receiver — a
|
|
21
|
+
* compromised Otto server could re-point `_receiver` inside a diamond swap; that residual is bounded by
|
|
22
|
+
* the per-swap cap, the grant's expiry and the user's revoke, not by the engine.
|
|
23
|
+
*
|
|
24
|
+
* IDENTITY OF THE FENCE (seat ruling D6): the policy is matched by its versioned DESCRIPTION and checked
|
|
25
|
+
* by a digest over its canonical rules on every read-back — never by a policy id in env (a re-created
|
|
26
|
+
* policy silently changes id; a renamed one silently changes shape; the digest catches both). The
|
|
27
|
+
* canonical form lowercases hex strings and sorts object keys, nothing else; if the engine ever
|
|
28
|
+
* augments rules on read-back, `canonical()` is the one place to teach it.
|
|
29
|
+
*
|
|
30
|
+
* Description obeys the SDK's regex (`/^[A-Za-z0-9 ,.]{1,50}$/`): no hyphens, no parentheses.
|
|
31
|
+
*/
|
|
32
|
+
import { createHash } from 'node:crypto';
|
|
33
|
+
import { getAddress } from 'viem';
|
|
34
|
+
import { refuse } from './execution-errors.js';
|
|
35
|
+
import { LIFI_DIAMOND_ADDRESS } from './execution-config.js';
|
|
36
|
+
/**
|
|
37
|
+
* The LINEAGE: a project policy is Otto's own only when its description matches the ANCHORED pattern
|
|
38
|
+
* `^OTTO model b swap fence v<N>$` (`modelBLineageVersion` returns N). The fence tool updates a lineage policy
|
|
39
|
+
* in place only when N ≤ this binary's version; a newer N is never downgraded; anything that does not match —
|
|
40
|
+
* the prefix in the middle, a suffix, no description — is foreign and is never touched.
|
|
41
|
+
*/
|
|
42
|
+
export const MODEL_B_POLICY_LINEAGE_PREFIX = 'OTTO model b swap fence';
|
|
43
|
+
/** This binary's fence version. Bump it whenever the rules change shape or caps. */
|
|
44
|
+
export const MODEL_B_POLICY_VERSION = 1;
|
|
45
|
+
/** The versioned match key. */
|
|
46
|
+
export const MODEL_B_POLICY_NAME_V1 = `${MODEL_B_POLICY_LINEAGE_PREFIX} v${MODEL_B_POLICY_VERSION}`;
|
|
47
|
+
const MODEL_B_LINEAGE_PATTERN = new RegExp(`^${MODEL_B_POLICY_LINEAGE_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')} v(\\d+)$`);
|
|
48
|
+
/** The lineage version N of a policy description, or undefined when the description is not of our lineage. */
|
|
49
|
+
export function modelBLineageVersion(description) {
|
|
50
|
+
if (typeof description !== 'string')
|
|
51
|
+
return undefined;
|
|
52
|
+
const match = MODEL_B_LINEAGE_PATTERN.exec(description);
|
|
53
|
+
return match ? Number(match[1]) : undefined;
|
|
54
|
+
}
|
|
55
|
+
export const MODEL_B_POLICY_NETWORK = 'base';
|
|
56
|
+
export const MODEL_B_CHAIN_ID = 8453;
|
|
57
|
+
/**
|
|
58
|
+
* Delegation expiry bounds (Founder word `expiry 90/180`, 2026-08-29): the mint page defaults to 90 days
|
|
59
|
+
* and refuses anything past 180; the server routes enforce the same numbers. Coinbase documents no
|
|
60
|
+
* maximum of its own for `expiresAt` (SDK types + vendor doc read 2026-08-29); the first 180-day mint
|
|
61
|
+
* at the sitting is the live confirmation.
|
|
62
|
+
*/
|
|
63
|
+
export const MODEL_B_DELEGATION_EXPIRY_DEFAULT_DAYS = 90;
|
|
64
|
+
export const MODEL_B_DELEGATION_EXPIRY_MAX_DAYS = 180;
|
|
65
|
+
/** A requested expiry, bounded: a hostile, absent or over-long value never reaches the mint. */
|
|
66
|
+
export function boundedDelegationExpiry(requestedDays, nowMs) {
|
|
67
|
+
const days = requestedDays === undefined || requestedDays === null
|
|
68
|
+
? MODEL_B_DELEGATION_EXPIRY_DEFAULT_DAYS
|
|
69
|
+
: Number(requestedDays);
|
|
70
|
+
if (!Number.isInteger(days) || days < 1 || days > MODEL_B_DELEGATION_EXPIRY_MAX_DAYS) {
|
|
71
|
+
throw new Error(`delegation expiry must be a whole number of days between 1 and ${MODEL_B_DELEGATION_EXPIRY_MAX_DAYS}`);
|
|
72
|
+
}
|
|
73
|
+
return Object.freeze({ days, expiresAt: new Date(nowMs + days * 86_400_000).toISOString() });
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The per-swap cap in USD — the Founder's number (word `fence cap 5000`, 2026-08-29). It bounds ONE swap:
|
|
77
|
+
* the engine carries no daily or total limit, and no copy may imply one. Changing it = a new fence version.
|
|
78
|
+
* Engine conditions are on raw calldata, so the cap is expressed per allowed input token: v1 allows USDC
|
|
79
|
+
* only, where the mapping is exact (5,000 USD = 5,000,000,000 atomic on the approve amount, which is what
|
|
80
|
+
* bounds the swap's `fromAmount` on-chain).
|
|
81
|
+
*/
|
|
82
|
+
export const MODEL_B_PER_SWAP_CAP_USD = 5000;
|
|
83
|
+
export const BASE_USDC = getAddress('0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913');
|
|
84
|
+
/**
|
|
85
|
+
* The input tokens a delegated swap may spend, v1: USDC only — the one token whose USD cap is exact.
|
|
86
|
+
* WETH (or any volatile input) needs a Founder-ruled reference price before it can carry a USD cap.
|
|
87
|
+
*/
|
|
88
|
+
export const MODEL_B_INPUT_TOKENS_V1 = Object.freeze([
|
|
89
|
+
Object.freeze({ symbol: 'USDC', address: BASE_USDC, decimals: 6, atomicPerUsd: 1000000n }),
|
|
90
|
+
]);
|
|
91
|
+
export function modelBInputToken(token) {
|
|
92
|
+
const target = getAddress(token);
|
|
93
|
+
return MODEL_B_INPUT_TOKENS_V1.find((entry) => entry.address === target);
|
|
94
|
+
}
|
|
95
|
+
/** The cap in the token's atomic units. */
|
|
96
|
+
export function modelBPerSwapCapAtomic(token, capUsd = MODEL_B_PER_SWAP_CAP_USD) {
|
|
97
|
+
if (!Number.isSafeInteger(capUsd) || capUsd <= 0)
|
|
98
|
+
throw new Error('per-swap cap must be a positive integer USD amount');
|
|
99
|
+
return BigInt(capUsd) * token.atomicPerUsd;
|
|
100
|
+
}
|
|
101
|
+
/** LibSwap.SwapData tuple, as the GenericSwapV3 facet declares it. */
|
|
102
|
+
const SWAP_DATA_COMPONENTS = [
|
|
103
|
+
{ name: 'callTo', type: 'address' },
|
|
104
|
+
{ name: 'approveTo', type: 'address' },
|
|
105
|
+
{ name: 'sendingAssetId', type: 'address' },
|
|
106
|
+
{ name: 'receivingAssetId', type: 'address' },
|
|
107
|
+
{ name: 'fromAmount', type: 'uint256' },
|
|
108
|
+
{ name: 'callData', type: 'bytes' },
|
|
109
|
+
{ name: 'requiresDeposit', type: 'bool' },
|
|
110
|
+
];
|
|
111
|
+
/** The two GenericSwapV3 ERC20→ERC20 entry points a same-chain swap plan may call. */
|
|
112
|
+
export const GENERIC_SWAP_V3_ABI = [
|
|
113
|
+
{
|
|
114
|
+
type: 'function',
|
|
115
|
+
name: 'swapTokensSingleV3ERC20ToERC20',
|
|
116
|
+
stateMutability: 'nonpayable',
|
|
117
|
+
inputs: [
|
|
118
|
+
{ name: '_transactionId', type: 'bytes32' },
|
|
119
|
+
{ name: '_integrator', type: 'string' },
|
|
120
|
+
{ name: '_referrer', type: 'string' },
|
|
121
|
+
{ name: '_receiver', type: 'address' },
|
|
122
|
+
{ name: '_minAmount', type: 'uint256' },
|
|
123
|
+
{ name: '_swapData', type: 'tuple', components: SWAP_DATA_COMPONENTS },
|
|
124
|
+
],
|
|
125
|
+
outputs: [],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
type: 'function',
|
|
129
|
+
name: 'swapTokensMultipleV3ERC20ToERC20',
|
|
130
|
+
stateMutability: 'nonpayable',
|
|
131
|
+
inputs: [
|
|
132
|
+
{ name: '_transactionId', type: 'bytes32' },
|
|
133
|
+
{ name: '_integrator', type: 'string' },
|
|
134
|
+
{ name: '_referrer', type: 'string' },
|
|
135
|
+
{ name: '_receiver', type: 'address' },
|
|
136
|
+
{ name: '_minAmount', type: 'uint256' },
|
|
137
|
+
{ name: '_swapData', type: 'tuple[]', components: SWAP_DATA_COMPONENTS },
|
|
138
|
+
],
|
|
139
|
+
outputs: [],
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
/** The static v1 rules: one approve rule per allow-listed input token, one router rule. */
|
|
143
|
+
export function modelBProjectPolicyRules(capUsd = MODEL_B_PER_SWAP_CAP_USD) {
|
|
144
|
+
const approveRules = MODEL_B_INPUT_TOKENS_V1.map((token) => ({
|
|
145
|
+
action: 'accept',
|
|
146
|
+
operation: 'sendEndUserEvmTransaction',
|
|
147
|
+
criteria: [
|
|
148
|
+
{ type: 'evmNetwork', networks: [MODEL_B_POLICY_NETWORK], operator: 'in' },
|
|
149
|
+
{ type: 'evmAddress', addresses: [token.address], operator: 'in' },
|
|
150
|
+
{ type: 'ethValue', ethValue: '0', operator: '<=' },
|
|
151
|
+
{
|
|
152
|
+
type: 'evmData',
|
|
153
|
+
abi: 'erc20',
|
|
154
|
+
conditions: [
|
|
155
|
+
{
|
|
156
|
+
function: 'approve',
|
|
157
|
+
params: [
|
|
158
|
+
{ name: 'spender', operator: 'in', values: [LIFI_DIAMOND_ADDRESS] },
|
|
159
|
+
{
|
|
160
|
+
name: 'value',
|
|
161
|
+
operator: '<=',
|
|
162
|
+
value: modelBPerSwapCapAtomic(token, capUsd).toString(),
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
}));
|
|
170
|
+
const routerRule = {
|
|
171
|
+
action: 'accept',
|
|
172
|
+
operation: 'sendEndUserEvmTransaction',
|
|
173
|
+
criteria: [
|
|
174
|
+
{ type: 'evmNetwork', networks: [MODEL_B_POLICY_NETWORK], operator: 'in' },
|
|
175
|
+
{ type: 'evmAddress', addresses: [LIFI_DIAMOND_ADDRESS], operator: 'in' },
|
|
176
|
+
{ type: 'ethValue', ethValue: '0', operator: '<=' },
|
|
177
|
+
{
|
|
178
|
+
type: 'evmData',
|
|
179
|
+
abi: GENERIC_SWAP_V3_ABI,
|
|
180
|
+
conditions: [
|
|
181
|
+
{ function: 'swapTokensSingleV3ERC20ToERC20' },
|
|
182
|
+
{ function: 'swapTokensMultipleV3ERC20ToERC20' },
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
};
|
|
187
|
+
return Object.freeze([...approveRules, routerRule]);
|
|
188
|
+
}
|
|
189
|
+
/** The body PR-2's installer creates or updates: scope, versioned description, the v1 rules. */
|
|
190
|
+
export function modelBProjectPolicyBody() {
|
|
191
|
+
return Object.freeze({
|
|
192
|
+
scope: 'project',
|
|
193
|
+
description: MODEL_B_POLICY_NAME_V1,
|
|
194
|
+
rules: modelBProjectPolicyRules(),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
/** Sorted keys, undefined dropped, hex strings lowercased; arrays keep their order. Nothing else. */
|
|
198
|
+
function canonical(value) {
|
|
199
|
+
if (Array.isArray(value))
|
|
200
|
+
return value.map(canonical);
|
|
201
|
+
if (value !== null && typeof value === 'object') {
|
|
202
|
+
const out = {};
|
|
203
|
+
for (const key of Object.keys(value).sort()) {
|
|
204
|
+
const item = value[key];
|
|
205
|
+
if (item !== undefined)
|
|
206
|
+
out[key] = canonical(item);
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
if (typeof value === 'string' && /^0x[0-9a-fA-F]+$/.test(value))
|
|
211
|
+
return value.toLowerCase();
|
|
212
|
+
return value;
|
|
213
|
+
}
|
|
214
|
+
/** SHA-256 over the canonical JSON of a rule list — the fence's integrity check at read-back. */
|
|
215
|
+
export function policyRulesDigest(rules) {
|
|
216
|
+
return createHash('sha256')
|
|
217
|
+
.update(JSON.stringify(canonical(rules)))
|
|
218
|
+
.digest('hex');
|
|
219
|
+
}
|
|
220
|
+
export const MODEL_B_POLICY_DIGEST_V1 = policyRulesDigest(modelBProjectPolicyRules());
|
|
221
|
+
/**
|
|
222
|
+
* The fence check the delegated-send path runs BEFORE any send: EXACTLY ONE project-scope policy reads back
|
|
223
|
+
* (Coinbase allows one; more is an anomalous snapshot and never blessed on the strength of "one of them
|
|
224
|
+
* matches"), it carries the versioned name, and its rules digest to exactly the v1 rules. Anything else is
|
|
225
|
+
* "absent" — including a policy of the right name whose rules drifted.
|
|
226
|
+
*/
|
|
227
|
+
export function readBackModelBFence(policies) {
|
|
228
|
+
const project = policies.filter((policy) => policy.scope === 'project');
|
|
229
|
+
if (project.length === 0)
|
|
230
|
+
return {
|
|
231
|
+
present: false,
|
|
232
|
+
reason: 'absent',
|
|
233
|
+
detail: 'the project holds no project-scope policy',
|
|
234
|
+
};
|
|
235
|
+
if (project.length > 1) {
|
|
236
|
+
return {
|
|
237
|
+
present: false,
|
|
238
|
+
reason: 'ambiguous',
|
|
239
|
+
detail: `${project.length} project-scope policies read back where Coinbase allows one`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
const named = project[0];
|
|
243
|
+
if (named.description !== MODEL_B_POLICY_NAME_V1) {
|
|
244
|
+
return {
|
|
245
|
+
present: false,
|
|
246
|
+
reason: 'name_mismatch',
|
|
247
|
+
detail: `the project policy is not "${MODEL_B_POLICY_NAME_V1}"`,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const rulesDigest = policyRulesDigest(named.rules);
|
|
251
|
+
if (rulesDigest !== MODEL_B_POLICY_DIGEST_V1) {
|
|
252
|
+
return {
|
|
253
|
+
present: false,
|
|
254
|
+
reason: 'digest_mismatch',
|
|
255
|
+
detail: `the project policy "${MODEL_B_POLICY_NAME_V1}" carries rules that do not digest to v1 (${rulesDigest.slice(0, 16)}… ≠ ${MODEL_B_POLICY_DIGEST_V1.slice(0, 16)}…)`,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
return { present: true, id: named.id, rulesDigest };
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* The same bound the engine enforces, checked server-side BEFORE the first step is sent: a plan the
|
|
262
|
+
* engine would refuse is refused here without spending an allowance reset on it.
|
|
263
|
+
*/
|
|
264
|
+
export function assertWithinModelBFence(plan, capUsd = MODEL_B_PER_SWAP_CAP_USD, inputTokens = MODEL_B_INPUT_TOKENS_V1) {
|
|
265
|
+
const target = getAddress(plan.token);
|
|
266
|
+
const token = inputTokens.find((entry) => entry.address === target);
|
|
267
|
+
if (!token) {
|
|
268
|
+
return refuse('DELEGATION_OUTSIDE_FENCE', `input token ${plan.token} is not an allow-listed delegated input (${inputTokens.map((t) => t.symbol).join(', ') || 'none'})`);
|
|
269
|
+
}
|
|
270
|
+
if (getAddress(plan.spender) !== LIFI_DIAMOND_ADDRESS) {
|
|
271
|
+
return refuse('DELEGATION_OUTSIDE_FENCE', `approval spender ${plan.spender} is not the reviewed LI.FI diamond`);
|
|
272
|
+
}
|
|
273
|
+
const capAtomic = modelBPerSwapCapAtomic(token, capUsd);
|
|
274
|
+
if (plan.gross > capAtomic) {
|
|
275
|
+
return refuse('DELEGATION_OUTSIDE_FENCE', `gross ${plan.gross} ${token.symbol} atomic exceeds the per-swap cap ${capAtomic} (${capUsd} USD)`);
|
|
276
|
+
}
|
|
277
|
+
return { token, capAtomic };
|
|
278
|
+
}
|
|
279
|
+
//# sourceMappingURL=execution-delegation-policy.js.map
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* execution-delegation.ts — the delegated-send path on the hosted MCP (Model B, browser plane, PR-1).
|
|
3
|
+
*
|
|
4
|
+
* The human minted a time-boxed CDP delegation in a browser (PR-2); this is what Otto's server does for
|
|
5
|
+
* that human's agent, headlessly. It holds Otto's developer-side CDP access (never a user key), and it
|
|
6
|
+
* submits ONLY a plan the user-side verifier accepts for THIS end user, under a fence that is READ BACK
|
|
7
|
+
* before any authority is exercised. Order per call, as a server invariant:
|
|
8
|
+
*
|
|
9
|
+
* 0. caller authentication — the per-request header, compared in constant time, BEFORE anything is
|
|
10
|
+
* parsed or read (a wrong caller learns nothing and causes no CDP read);
|
|
11
|
+
* 1. the artifact was PREPARED BY THIS PROCESS (`prepared-artifacts.ts`) — else refuse; then the
|
|
12
|
+
* per-artifact idempotency record (sent → replayed, halted → never retried), then one delegated
|
|
13
|
+
* plan at a time per end user (the rig lock serialises revocation, not sends);
|
|
14
|
+
* 2. FENCE: the project policy is present, named and digests to v1 — else refuse;
|
|
15
|
+
* 3. GRANT: the user's delegation reads back active past the envelope's own expiry — else refuse;
|
|
16
|
+
* 4. ACCOUNT: the address the caller prepared for is one of THIS end user's CDP accounts, as CDP reports
|
|
17
|
+
* them — never an address the caller binds on its own (a Dynamic-connected address is never a CDP
|
|
18
|
+
* end-user address by construction);
|
|
19
|
+
* 5. VERIFY: the 13-check verifier over the envelope, for exactly the request that produced it, with the
|
|
20
|
+
* server-confirmed address; then the fence's own bound (allow-listed token, reviewed spender, cap);
|
|
21
|
+
* 6. SEND: `CdpDelegatedSigner.sendPlan` — sealed receipts, halt clears the allowance, fail-closed once
|
|
22
|
+
* revocation was requested.
|
|
23
|
+
*
|
|
24
|
+
* Nothing here logs the header, the secret, or a credential. Refusals are machine-readable codes; upstream
|
|
25
|
+
* error bodies are never relayed.
|
|
26
|
+
*/
|
|
27
|
+
import { type Address, type PublicClient } from 'viem';
|
|
28
|
+
import type { SealedWaitOptions } from './adapter/chain.js';
|
|
29
|
+
import { DelegatedPlanHaltedError, type CdpDeveloperCredentials, type CdpRig, type DelegatedHaltCause } from './adapter/cdp-signer.js';
|
|
30
|
+
import { type DelegatedSendResult } from './execution-delegated-definition.js';
|
|
31
|
+
import { type DelegationPolicyRecord, type ModelBInputToken } from './execution-delegation-policy.js';
|
|
32
|
+
import { ExecutionRefusal } from './execution-errors.js';
|
|
33
|
+
import type { ExecutionToolRuntime } from './execution-tools.js';
|
|
34
|
+
export declare const DELEGATION_ENV_NAMES: readonly ["CDP_API_KEY_ID", "CDP_API_KEY_SECRET", "CDP_WALLET_SECRET", "OTTO_DELEGATION_SERVER_SECRET", "BASE_RPC_URL"];
|
|
35
|
+
/** The public CDP project id the fence-status tool publishes (a UUID; PUBLIC, never a secret). */
|
|
36
|
+
export declare const PUBLIC_PROJECT_ID_ENV_NAME: "OTTO_CDP_PROJECT_ID";
|
|
37
|
+
export interface DelegationEnv {
|
|
38
|
+
readonly credentials: CdpDeveloperCredentials;
|
|
39
|
+
readonly serverSecret: string;
|
|
40
|
+
readonly rpcUrl: string;
|
|
41
|
+
/** Otto's PUBLIC CDP project id (a UUID), when `OTTO_CDP_PROJECT_ID` is set; else undefined. */
|
|
42
|
+
readonly publicProjectId?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* All five absent → the delegated tool does not exist (same shape as the OTTO_EXECUTION_MCP_ENABLED
|
|
46
|
+
* switch). Any present but not all → boot refuses, naming the MISSING variables (never a value).
|
|
47
|
+
*/
|
|
48
|
+
export declare function loadDelegationEnv(env?: NodeJS.ProcessEnv): DelegationEnv | undefined;
|
|
49
|
+
export interface DelegationEndUserRecord {
|
|
50
|
+
readonly userId: string;
|
|
51
|
+
readonly evmAddresses: readonly Address[];
|
|
52
|
+
}
|
|
53
|
+
/** What the delegated-send path needs from CDP beyond the fenced signer rig: the fence and the user. */
|
|
54
|
+
export interface DelegationRig {
|
|
55
|
+
/** The fenced rig `CdpDelegatedSigner` runs over (reads + revocation; no raw submission surface). */
|
|
56
|
+
readonly rig: CdpRig;
|
|
57
|
+
listProjectPolicies(): Promise<readonly DelegationPolicyRecord[]>;
|
|
58
|
+
/** `undefined` when CDP has no such end user. */
|
|
59
|
+
readEndUser(userId: string): Promise<DelegationEndUserRecord | undefined>;
|
|
60
|
+
/** Create THE project-scope policy (Coinbase allows one); returns its id. Only the fence tool calls this. */
|
|
61
|
+
createProjectPolicy(body: DelegationProjectPolicyBody): Promise<string>;
|
|
62
|
+
/** Update an existing project-scope policy in place (description + rules). Only the fence tool calls this. */
|
|
63
|
+
updateProjectPolicy(id: string, body: DelegationProjectPolicyBody): Promise<void>;
|
|
64
|
+
/** One policy by id, as CDP reports it now; `undefined` when it no longer exists. The fence tool's pre-write re-read. */
|
|
65
|
+
readProjectPolicy(id: string): Promise<DelegationPolicyRecord | undefined>;
|
|
66
|
+
/** The end user a CDP access token resolves to, or `undefined` when the token is not accepted. Never logged. */
|
|
67
|
+
validateAccessToken(accessToken: string): Promise<DelegationEndUserRecord | undefined>;
|
|
68
|
+
}
|
|
69
|
+
export interface DelegationProjectPolicyBody {
|
|
70
|
+
readonly scope: 'project';
|
|
71
|
+
readonly description: string;
|
|
72
|
+
readonly rules: unknown;
|
|
73
|
+
}
|
|
74
|
+
/** The subset of `CdpClient` the delegated-send path touches — structural, so no SDK import is needed to type it. */
|
|
75
|
+
export interface DelegationCdpClient {
|
|
76
|
+
readonly endUser: {
|
|
77
|
+
getEndUser(options: {
|
|
78
|
+
userId: string;
|
|
79
|
+
}): Promise<{
|
|
80
|
+
readonly userId: string;
|
|
81
|
+
readonly evmAccounts?: readonly string[];
|
|
82
|
+
readonly evmAccountObjects?: readonly {
|
|
83
|
+
readonly address: string;
|
|
84
|
+
}[];
|
|
85
|
+
}>;
|
|
86
|
+
getDelegationForEndUser(options: {
|
|
87
|
+
userId: string;
|
|
88
|
+
}): Promise<{
|
|
89
|
+
readonly expiresAt: string;
|
|
90
|
+
}>;
|
|
91
|
+
revokeDelegationForEndUser(options: {
|
|
92
|
+
userId: string;
|
|
93
|
+
}): Promise<void>;
|
|
94
|
+
validateAccessToken(options: {
|
|
95
|
+
accessToken: string;
|
|
96
|
+
}): Promise<{
|
|
97
|
+
readonly userId: string;
|
|
98
|
+
readonly evmAccounts?: readonly string[];
|
|
99
|
+
readonly evmAccountObjects?: readonly {
|
|
100
|
+
readonly address: string;
|
|
101
|
+
}[];
|
|
102
|
+
}>;
|
|
103
|
+
sendEvmTransaction(options: {
|
|
104
|
+
userId: string;
|
|
105
|
+
address: string;
|
|
106
|
+
transaction: `0x${string}`;
|
|
107
|
+
network: 'base';
|
|
108
|
+
}): Promise<{
|
|
109
|
+
readonly transactionHash: string;
|
|
110
|
+
}>;
|
|
111
|
+
};
|
|
112
|
+
readonly policies: {
|
|
113
|
+
listPolicies(options: {
|
|
114
|
+
scope: 'project';
|
|
115
|
+
}): Promise<{
|
|
116
|
+
readonly policies?: readonly DelegationPolicyRecord[];
|
|
117
|
+
}>;
|
|
118
|
+
createPolicy(options: {
|
|
119
|
+
policy: DelegationProjectPolicyBody;
|
|
120
|
+
}): Promise<{
|
|
121
|
+
readonly id: string;
|
|
122
|
+
}>;
|
|
123
|
+
updatePolicy(options: {
|
|
124
|
+
id: string;
|
|
125
|
+
policy: {
|
|
126
|
+
readonly description: string;
|
|
127
|
+
readonly rules: unknown;
|
|
128
|
+
};
|
|
129
|
+
}): Promise<unknown>;
|
|
130
|
+
getPolicyById(options: {
|
|
131
|
+
id: string;
|
|
132
|
+
}): Promise<DelegationPolicyRecord>;
|
|
133
|
+
/** Declared so a mock can PROVE it is never called; this service never invokes it. */
|
|
134
|
+
deletePolicy?(options: {
|
|
135
|
+
id: string;
|
|
136
|
+
}): Promise<void>;
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/** Build the delegation rig over a CDP client (real or structural mock) and a Base public client. */
|
|
140
|
+
export declare function createDelegationRig(cdp: DelegationCdpClient, pub: PublicClient): DelegationRig;
|
|
141
|
+
export interface DelegationServices {
|
|
142
|
+
readonly rig: DelegationRig;
|
|
143
|
+
/** The server-to-server secret the caller must present in `x-otto-delegation-auth`. Never logged. */
|
|
144
|
+
readonly serverSecret: string;
|
|
145
|
+
/** Otto's PUBLIC CDP project id (a UUID) the fence-status tool publishes; undefined disables that tool. */
|
|
146
|
+
readonly publicProjectId?: string;
|
|
147
|
+
readonly sealedWait?: SealedWaitOptions;
|
|
148
|
+
/**
|
|
149
|
+
* Test-only injection of the server-side fence bound (the cap / the input allow-list), so a test can
|
|
150
|
+
* prove the tool CALLS the bound with a plan the fixtures can build — an assertion no test can redden is
|
|
151
|
+
* indistinguishable from an absent one. Production boot (`createDelegationServicesFromEnv`) never sets
|
|
152
|
+
* it; the engine's installed policy is the v1 constant either way.
|
|
153
|
+
*/
|
|
154
|
+
readonly fenceBound?: {
|
|
155
|
+
readonly perSwapCapUsd?: number;
|
|
156
|
+
readonly inputTokens?: readonly ModelBInputToken[];
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
export interface DelegationCaller {
|
|
160
|
+
/** The header value as presented, or undefined when absent. Never logged. */
|
|
161
|
+
readonly presentedSecret?: string;
|
|
162
|
+
}
|
|
163
|
+
/** Pull the caller's presented secret out of the MCP request headers (case-insensitive, first value). */
|
|
164
|
+
export declare function callerFromHeaders(headers: Readonly<Record<string, string | string[] | undefined>> | undefined): DelegationCaller;
|
|
165
|
+
/**
|
|
166
|
+
* The caller check EVERY delegation tool runs first — constant-time, before parsing or any CDP read. One
|
|
167
|
+
* seam for the send tool and the admin tools (`execution-delegation-admin.ts`).
|
|
168
|
+
*/
|
|
169
|
+
export declare function assertDelegationCaller(services: DelegationServices, caller: DelegationCaller): void;
|
|
170
|
+
/**
|
|
171
|
+
* The cause class of a halt: the signer's STRUCTURED `cause` only (set from the CDP `errorType`, an error's
|
|
172
|
+
* class or the receipt status at the signer's catch sites). The message text — which embeds upstream
|
|
173
|
+
* text — is never consulted, so upstream text can neither leak nor select the class.
|
|
174
|
+
*/
|
|
175
|
+
export declare function haltCause(error: DelegatedPlanHaltedError): DelegatedHaltCause;
|
|
176
|
+
/** Map a verifier / signer failure to the tool's refusal taxonomy; anything else is rethrown. */
|
|
177
|
+
export declare function toDelegationRefusal(error: unknown): ExecutionRefusal | undefined;
|
|
178
|
+
/**
|
|
179
|
+
* Submit a prepared swap under the end user's delegation — the order in the file header is the contract.
|
|
180
|
+
* `caller` is derived from the request headers by the registration layer; it never comes from arguments.
|
|
181
|
+
*/
|
|
182
|
+
export declare function submitUnderDelegation(runtime: ExecutionToolRuntime, rawInput: unknown, caller: DelegationCaller, _signal?: AbortSignal): Promise<DelegatedSendResult>;
|
|
183
|
+
/**
|
|
184
|
+
* Open the delegation services from a loaded environment: the SDK client over Otto's developer
|
|
185
|
+
* credentials, a Base public client whose chain id is checked once at boot, and the fenced rig.
|
|
186
|
+
* Nothing here reads a policy, a grant or a user — every call does that itself, in order.
|
|
187
|
+
*/
|
|
188
|
+
export declare function openDelegationServicesFromEnv(env: DelegationEnv): Promise<DelegationServices>;
|
|
189
|
+
/** `loadDelegationEnv` + `openDelegationServicesFromEnv`; undefined when the five variables are all absent. */
|
|
190
|
+
export declare function createDelegationServicesFromEnv(env?: NodeJS.ProcessEnv): Promise<DelegationServices | undefined>;
|