otto-execute 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.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +151 -0
  3. package/dist/cdp-signer.d.ts +2 -0
  4. package/dist/cdp-signer.js +3 -0
  5. package/dist/cdp.d.ts +7 -0
  6. package/dist/cdp.js +8 -0
  7. package/dist/chain.d.ts +2 -0
  8. package/dist/chain.js +3 -0
  9. package/dist/cli.d.ts +17 -0
  10. package/dist/cli.js +400 -0
  11. package/dist/delegate.d.ts +117 -0
  12. package/dist/delegate.js +394 -0
  13. package/dist/eoa-signer.d.ts +94 -0
  14. package/dist/eoa-signer.js +289 -0
  15. package/dist/erc20.d.ts +2 -0
  16. package/dist/erc20.js +3 -0
  17. package/dist/index.d.ts +16 -0
  18. package/dist/index.js +19 -0
  19. package/dist/lifi-decode.d.ts +2 -0
  20. package/dist/lifi-decode.js +3 -0
  21. package/dist/mutations.d.ts +55 -0
  22. package/dist/mutations.js +363 -0
  23. package/dist/refusal.d.ts +2 -0
  24. package/dist/refusal.js +3 -0
  25. package/dist/vendor/otto-intel-mcp/VENDORED.json +52 -0
  26. package/dist/vendor/otto-intel-mcp/adapter/cdp-signer.d.ts +133 -0
  27. package/dist/vendor/otto-intel-mcp/adapter/cdp-signer.js +356 -0
  28. package/dist/vendor/otto-intel-mcp/adapter/chain.d.ts +36 -0
  29. package/dist/vendor/otto-intel-mcp/adapter/chain.js +65 -0
  30. package/dist/vendor/otto-intel-mcp/adapter/erc20.d.ts +39 -0
  31. package/dist/vendor/otto-intel-mcp/adapter/erc20.js +17 -0
  32. package/dist/vendor/otto-intel-mcp/adapter/lifi-decode.d.ts +52 -0
  33. package/dist/vendor/otto-intel-mcp/adapter/lifi-decode.js +149 -0
  34. package/dist/vendor/otto-intel-mcp/adapter/refusal.d.ts +21 -0
  35. package/dist/vendor/otto-intel-mcp/adapter/refusal.js +55 -0
  36. package/dist/vendor/otto-intel-mcp/adapter/sent-step.d.ts +15 -0
  37. package/dist/vendor/otto-intel-mcp/adapter/sent-step.js +6 -0
  38. package/dist/vendor/otto-intel-mcp/adapter/verify.d.ts +149 -0
  39. package/dist/vendor/otto-intel-mcp/adapter/verify.js +432 -0
  40. package/dist/vendor/otto-intel-mcp/adapter-cdp-index.d.ts +6 -0
  41. package/dist/vendor/otto-intel-mcp/adapter-cdp-index.js +7 -0
  42. package/dist/vendor/otto-intel-mcp/adapter-index.d.ts +15 -0
  43. package/dist/vendor/otto-intel-mcp/adapter-index.js +15 -0
  44. package/dist/vendor/otto-intel-mcp/artifact-id.d.ts +16 -0
  45. package/dist/vendor/otto-intel-mcp/artifact-id.js +60 -0
  46. package/dist/vendor/otto-intel-mcp/execution-config.d.ts +232 -0
  47. package/dist/vendor/otto-intel-mcp/execution-config.js +443 -0
  48. package/dist/vendor/otto-intel-mcp/execution-delegated-definition.d.ts +165 -0
  49. package/dist/vendor/otto-intel-mcp/execution-delegated-definition.js +116 -0
  50. package/dist/vendor/otto-intel-mcp/execution-delegation-admin-definition.d.ts +208 -0
  51. package/dist/vendor/otto-intel-mcp/execution-delegation-admin-definition.js +170 -0
  52. package/dist/vendor/otto-intel-mcp/execution-delegation-policy.d.ts +257 -0
  53. package/dist/vendor/otto-intel-mcp/execution-delegation-policy.js +279 -0
  54. package/dist/vendor/otto-intel-mcp/execution-errors.d.ts +9 -0
  55. package/dist/vendor/otto-intel-mcp/execution-errors.js +134 -0
  56. package/dist/vendor/otto-intel-mcp/execution-index.d.ts +17 -0
  57. package/dist/vendor/otto-intel-mcp/execution-index.js +16 -0
  58. package/dist/vendor/otto-intel-mcp/execution-intent.d.ts +14 -0
  59. package/dist/vendor/otto-intel-mcp/execution-intent.js +36 -0
  60. package/dist/vendor/otto-intel-mcp/execution-tool-definitions.d.ts +1103 -0
  61. package/dist/vendor/otto-intel-mcp/execution-tool-definitions.js +1051 -0
  62. package/dist/vendor/otto-intel-mcp/execution-types.d.ts +274 -0
  63. package/dist/vendor/otto-intel-mcp/execution-types.js +157 -0
  64. package/dist/verify.d.ts +2 -0
  65. package/dist/verify.js +3 -0
  66. package/dist/x402-table.d.ts +99 -0
  67. package/dist/x402-table.js +221 -0
  68. package/package.json +97 -0
@@ -0,0 +1,289 @@
1
+ /**
2
+ * eoa-signer.ts — the bring-your-own-EOA capability provider (spec §2 SigningAdapter, §9 handshakes).
3
+ *
4
+ * Provides `send_evm_transaction` (one signed tx per plan step, in order; nonce/gas/serialization are
5
+ * adapter-owned per spec §6) and `sign_x402_payment` (challenge → signed `x_payment`, exactly the
6
+ * hosted MCP's existing retry protocol). The key never leaves this process; it is read from the
7
+ * environment once (env.ts registers it with the output guard) or generated in-process for a
8
+ * dry run and discarded. On this path the USER's key both signs and submits.
9
+ *
10
+ * Dry run = sign, never broadcast: every step is signed with placeholder nonce/gas, parsed back, and
11
+ * the parsed {to, value, data} must equal the verified plan step byte-for-byte, and the signature
12
+ * must recover to this account. That proves "what the signer signs is what the adapter verified"
13
+ * without touching a chain.
14
+ */
15
+ import { createPublicClient, encodeFunctionData, getAddress, http, parseTransaction, recoverTransactionAddress, } from 'viem';
16
+ import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
17
+ import { base } from 'viem/chains';
18
+ import { waitForSealedReceipt } from './chain.js';
19
+ import { ERC20_APPROVE_ABI, readAllowance } from './erc20.js';
20
+ import { x402Client } from '@x402/core/client';
21
+ import { encodePaymentSignatureHeader } from '@x402/core/http';
22
+ import { ExactEvmScheme } from '@x402/evm/exact/client';
23
+ import { toClientEvmSigner } from '@x402/evm';
24
+ import { requirementMatchesRow, validateX402Challenge, X402_TABLE, X402Refusal, } from './x402-table.js';
25
+ import { assertPlanSignableBy } from './verify.js';
26
+ export const EOA_CAPABILITIES = Object.freeze([
27
+ 'send_evm_transaction',
28
+ 'sign_x402_payment',
29
+ ]);
30
+ export const CHAIN_ID = 8453;
31
+ /** Clock tolerance when bounding an EIP-3009 `validBefore` against the row's window. */
32
+ const VALID_BEFORE_SKEW_SECONDS = 60;
33
+ /**
34
+ * The INDEPENDENT per-payment ceiling applied to every x402 signature, stated here rather than
35
+ * inherited from `@x402/core`'s default (which is the same `$1`, but is the SDK's number to change,
36
+ * not ours). Every live table row is $0.001–$0.002, so this has ~500x of headroom over real traffic;
37
+ * it exists to bound a table that is wrong, not to bound normal use. Raising it is a deliberate edit.
38
+ */
39
+ export const PER_PAYMENT_SPEND_CAP = '$1';
40
+ /**
41
+ * True only for `@x402/core`'s spend-control rejection. Matched narrowly on the SDK's own message so a
42
+ * different failure is never mistaken for a budget refusal; if the SDK ever rewords this, the
43
+ * mutation-proved test below fails loudly rather than the translation silently going quiet.
44
+ */
45
+ function isSpendControlRejection(error) {
46
+ return error instanceof Error && /rejected by spendControls/.test(error.message);
47
+ }
48
+ /** A plan step failed after an approval was live; the adapter tried to clear the allowance. */
49
+ export class PlanHaltedError extends Error {
50
+ failedStep;
51
+ allowanceCleared;
52
+ clearTransactionHash;
53
+ constructor(message, failedStep, allowanceCleared, clearTransactionHash) {
54
+ super(message);
55
+ this.failedStep = failedStep;
56
+ this.allowanceCleared = allowanceCleared;
57
+ this.clearTransactionHash = clearTransactionHash;
58
+ this.name = 'PlanHaltedError';
59
+ }
60
+ }
61
+ export class EoaSigner {
62
+ account;
63
+ rpcUrl;
64
+ address;
65
+ capabilities = EOA_CAPABILITIES;
66
+ chainId = CHAIN_ID;
67
+ constructor(account, rpcUrl) {
68
+ this.account = account;
69
+ this.rpcUrl = rpcUrl;
70
+ this.address = getAddress(account.address);
71
+ }
72
+ static fromPrivateKey(privateKey, rpcUrl) {
73
+ return new EoaSigner(privateKeyToAccount(privateKey), rpcUrl);
74
+ }
75
+ /** A throwaway in-process key for dry runs. Never printed, never persisted, gone with the process. */
76
+ static ephemeral() {
77
+ return new EoaSigner(privateKeyToAccount(generatePrivateKey()), undefined);
78
+ }
79
+ publicClient() {
80
+ return createPublicClient({ chain: base, transport: http(this.rpcUrl) });
81
+ }
82
+ /**
83
+ * `send_evm_transaction`, dry: sign every step, decode back, recover — broadcast nothing. `clock` is
84
+ * either a fixed instant (deterministic tests) or a clock FUNCTION read afresh before every signature
85
+ * (the default, `Date.now`), so an artifact that expires between two signatures is caught.
86
+ */
87
+ async signPlanDryRun(plan, clock = Date.now) {
88
+ const now = typeof clock === 'number' ? () => clock : clock;
89
+ assertPlanSignableBy(plan, this, now());
90
+ const out = [];
91
+ for (const [index, step] of plan.steps.entries()) {
92
+ // Re-asserted immediately before each signature, from a fresh clock read, exactly as the live path does.
93
+ assertPlanSignableBy(plan, this, now());
94
+ const raw = await this.account.signTransaction({
95
+ chainId: CHAIN_ID,
96
+ type: 'eip1559',
97
+ nonce: index,
98
+ to: step.to,
99
+ value: step.value,
100
+ data: step.data,
101
+ gas: 400000n,
102
+ maxFeePerGas: 1000000n,
103
+ maxPriorityFeePerGas: 1000n,
104
+ });
105
+ const parsed = parseTransaction(raw);
106
+ if (parsed.type !== 'eip1559' ||
107
+ parsed.chainId !== CHAIN_ID ||
108
+ !parsed.to ||
109
+ getAddress(parsed.to) !== step.to ||
110
+ (parsed.value ?? 0n) !== step.value ||
111
+ (parsed.data ?? '0x').toLowerCase() !== step.data.toLowerCase()) {
112
+ throw new Error(`dry-run step ${index} (${step.kind}) did not decode back to the verified plan step`);
113
+ }
114
+ const recovered = getAddress(await recoverTransactionAddress({ serializedTransaction: raw }));
115
+ if (recovered !== this.address) {
116
+ throw new Error(`dry-run step ${index} recovered to ${recovered}, not ${this.address}`);
117
+ }
118
+ out.push(Object.freeze({
119
+ index,
120
+ kind: step.kind,
121
+ to: step.to,
122
+ signed_bytes: (raw.length - 2) / 2,
123
+ recovered_signer: recovered,
124
+ decoded_matches_plan: true,
125
+ broadcast: false,
126
+ }));
127
+ }
128
+ return out;
129
+ }
130
+ /**
131
+ * Nonce / fee / gas round trips first, then `beforeSign` (the plan's freshness + binding re-check,
132
+ * AFTER those round trips and immediately before the signature), then sign and broadcast. Resolves
133
+ * only with the SEALED, CANONICAL receipt (`waitForSealedReceipt`): the next step's gas estimate and
134
+ * any read-back must see this transaction, and a Flashblocks preconfirmation alone guarantees neither
135
+ * that they do nor that the transaction survived sealing.
136
+ */
137
+ async submit(pub, step, beforeSign, sealedWait) {
138
+ const [nonce, fees, gasEstimate] = await Promise.all([
139
+ pub.getTransactionCount({ address: this.address, blockTag: 'pending' }),
140
+ pub.estimateFeesPerGas(),
141
+ pub.estimateGas({ account: this.address, to: step.to, value: step.value, data: step.data }),
142
+ ]);
143
+ beforeSign?.();
144
+ const raw = await this.account.signTransaction({
145
+ chainId: CHAIN_ID,
146
+ type: 'eip1559',
147
+ nonce,
148
+ to: step.to,
149
+ value: step.value,
150
+ data: step.data,
151
+ gas: (gasEstimate * 12n) / 10n,
152
+ maxFeePerGas: fees.maxFeePerGas,
153
+ maxPriorityFeePerGas: fees.maxPriorityFeePerGas,
154
+ });
155
+ const hash = await pub.sendRawTransaction({ serializedTransaction: raw });
156
+ await pub.waitForTransactionReceipt({ hash }); // first sight (may be a preconfirmation) …
157
+ const receipt = await waitForSealedReceipt(pub, hash, sealedWait); // … then only the sealed canonical receipt counts
158
+ return { hash, status: receipt.status, blockNumber: receipt.blockNumber, gasUsed: receipt.gasUsed };
159
+ }
160
+ /**
161
+ * `send_evm_transaction`, live: one tx per step, in order, the plan re-checked before every step AND
162
+ * again immediately before each signature (after the nonce / fee / gas round trips, so an artifact
163
+ * that expires during preparation is never signed). The plan halts at the first step that does not
164
+ * succeed; after ANY failure a bounded `approve(spender, 0)` is attempted and its real outcome —
165
+ * confirmed by an allowance read-back — is reported, so no allowance outlives the halted plan unnoticed.
166
+ */
167
+ async sendPlan(plan, pub = this.publicClient(), sealedWait) {
168
+ assertPlanSignableBy(plan, this, Date.now());
169
+ const out = [];
170
+ for (const [index, step] of plan.steps.entries()) {
171
+ // Freshness is re-checked before EVERY live step: an artifact that expires mid-plan halts (and
172
+ // clears the allowance) instead of sending its remaining steps.
173
+ try {
174
+ assertPlanSignableBy(plan, this, Date.now());
175
+ }
176
+ catch (error) {
177
+ throw await this.halt(plan, pub, index, `plan no longer signable before step ${index}: ${error instanceof Error ? error.message : String(error)}`, sealedWait);
178
+ }
179
+ let result;
180
+ try {
181
+ result = await this.submit(pub, step, () => assertPlanSignableBy(plan, this, Date.now()), sealedWait);
182
+ }
183
+ catch (error) {
184
+ throw await this.halt(plan, pub, index, `step ${index} (${step.kind}) could not be submitted: ${error instanceof Error ? error.message : String(error)}`, sealedWait);
185
+ }
186
+ if (result.status !== 'success') {
187
+ throw await this.halt(plan, pub, index, `step ${index} (${step.kind}) reverted on-chain: ${result.hash}`, sealedWait);
188
+ }
189
+ out.push(Object.freeze({
190
+ index,
191
+ kind: step.kind,
192
+ to: step.to,
193
+ transaction_hash: result.hash,
194
+ status: 'success',
195
+ block_number: result.blockNumber.toString(),
196
+ gas_used: result.gasUsed.toString(),
197
+ }));
198
+ }
199
+ return out;
200
+ }
201
+ async halt(plan, pub, failedStep, message, sealedWait) {
202
+ // The clear is attempted after ANY failure, step 0 included: a failed reset may leave a
203
+ // PRE-EXISTING allowance, and from step 1 on the approval may already be broadcast even when the
204
+ // receipt or the transport failed. Clearing a never-set allowance is a harmless approve(spender, 0),
205
+ // and `allowanceCleared` reports the clear's real outcome, never an assumption.
206
+ const handOff = `clear approve(${plan.spender}, 0) on ${plan.token} by hand`;
207
+ let hash;
208
+ try {
209
+ const clear = await this.submit(pub, { to: plan.token, value: 0n, data: encodeFunctionData({ abi: ERC20_APPROVE_ABI, functionName: 'approve', args: [plan.spender, 0n] }) }, undefined, sealedWait);
210
+ hash = clear.hash;
211
+ if (clear.status !== 'success') {
212
+ return new PlanHaltedError(`${message}; allowance clear ${hash} reverted — ${handOff}`, failedStep, false, hash);
213
+ }
214
+ // A mined clear is a claim about a transaction; the read-back is the fact about the token.
215
+ const remaining = await readAllowance(pub, plan.token, this.address, plan.spender);
216
+ if (remaining !== 0n) {
217
+ return new PlanHaltedError(`${message}; allowance clear ${hash} was mined but ${remaining} still reads back — ${handOff}`, failedStep, false, hash);
218
+ }
219
+ return new PlanHaltedError(`${message}; allowance cleared in ${hash} (read-back 0)`, failedStep, true, hash);
220
+ }
221
+ catch (error) {
222
+ const reason = error instanceof Error ? error.message : String(error);
223
+ return new PlanHaltedError(`${message}; allowance clear ${hash ? `${hash} could not be confirmed` : 'FAILED'} (${reason}) — ${handOff}`, failedStep, false, hash);
224
+ }
225
+ }
226
+ /**
227
+ * `sign_x402_payment`: turn a table-validated challenge into the signed `x_payment` value.
228
+ * Validation and hand-off are two moments. Immediately before signing, the document is validated
229
+ * AGAIN against the static table and must resolve to the same row and the same bound fields as the
230
+ * earlier validation; the requirement selector accepts only a candidate matching that ROW; and the
231
+ * produced payload's `accepted` requirement and authorization are checked against the ROW (not
232
+ * against anything the challenge supplied) before the header is returned.
233
+ */
234
+ async signX402Payment(paymentRequired, validated, table = X402_TABLE, nowSeconds = Math.floor(Date.now() / 1000)) {
235
+ const fresh = validateX402Challenge(paymentRequired, table);
236
+ if (fresh.row !== validated.row || JSON.stringify(fresh.requirement) !== JSON.stringify(validated.requirement)) {
237
+ throw new Error('the challenge no longer validates to the same row and requirement it was validated against; refusing to sign');
238
+ }
239
+ const row = validated.row;
240
+ const signer = toClientEvmSigner(this.account, this.publicClient());
241
+ const client = new x402Client((_version, requirements) => {
242
+ const match = requirements.find((candidate) => requirementMatchesRow(candidate, row));
243
+ if (!match)
244
+ throw new Error('no offered requirement matches the validated table row');
245
+ return match;
246
+ });
247
+ // `@x402/core` >= 2.23 seeds `spendControls = {}` in the bare constructor, which the SDK reads as
248
+ // "default assets only, capped at $1 per payment". We state that cap EXPLICITLY rather than inherit
249
+ // it, so the number is visible in this file and a future SDK default cannot move it silently.
250
+ //
251
+ // It is deliberately KEPT, not disabled. The table checks more FIELDS, but it is not an independent
252
+ // authority: `table` is a parameter, and even the default table's `amount_atomic` values are
253
+ // hand-edited constants that nothing else cross-checks. A price typo (5000 -> 5000000) makes the
254
+ // table agree with itself perfectly, and only this cap — derived from the requirement amount rather
255
+ // than from the table — stands between that typo and a signature for 1000x the intended amount.
256
+ // Field coverage is not the same as a second opinion.
257
+ client.setSpendControls({ maxAmountPerPayment: PER_PAYMENT_SPEND_CAP });
258
+ client.register('eip155:*', new ExactEvmScheme(signer));
259
+ let payload;
260
+ try {
261
+ payload = (await client.createPaymentPayload(paymentRequired));
262
+ }
263
+ catch (error) {
264
+ // Translate ONLY the spend-control rejection into this package's typed refusal vocabulary, so a
265
+ // caller keying on X402Refusal sees a code rather than an SDK string. Anything else rethrows
266
+ // untouched — this must never become a catch-all that turns a real failure into a tidy refusal.
267
+ if (isSpendControlRejection(error)) {
268
+ throw new X402Refusal('X402_SPEND_LIMIT_EXCEEDED', 'amount', `at most ${PER_PAYMENT_SPEND_CAP} per payment`, `${row.amount_atomic} atomic units of ${row.asset}`, table.table_version);
269
+ }
270
+ throw error;
271
+ }
272
+ if (!requirementMatchesRow(payload.accepted, row)) {
273
+ throw new Error('payment payload accepted a requirement that does not match the validated table row');
274
+ }
275
+ const inner = (payload.payload ?? {});
276
+ const authorization = inner.authorization ?? {};
277
+ if (typeof authorization.from !== 'string' || getAddress(authorization.from) !== this.address ||
278
+ typeof authorization.to !== 'string' || getAddress(authorization.to) !== row.pay_to ||
279
+ authorization.value !== row.amount_atomic) {
280
+ throw new Error('payment authorization does not bind this signer to the table pay_to and amount');
281
+ }
282
+ const validBefore = Number(authorization.validBefore);
283
+ if (!Number.isFinite(validBefore) || validBefore > nowSeconds + row.max_timeout_seconds + VALID_BEFORE_SKEW_SECONDS) {
284
+ throw new Error(`payment authorization validBefore ${String(authorization.validBefore)} exceeds the row window of ${row.max_timeout_seconds}s`);
285
+ }
286
+ return Object.freeze({ header: encodePaymentSignatureHeader(payload), payload });
287
+ }
288
+ }
289
+ //# sourceMappingURL=eoa-signer.js.map
@@ -0,0 +1,2 @@
1
+ /** erc20.ts — re-exported from `./vendor/otto-intel-mcp/adapter-index.js` (moved 2026-08-29; see that entry's header). */
2
+ export { ERC20_ALLOWANCE_ABI, ERC20_APPROVE_ABI, readAllowance } from './vendor/otto-intel-mcp/adapter-index.js';
package/dist/erc20.js ADDED
@@ -0,0 +1,3 @@
1
+ /** erc20.ts — re-exported from `./vendor/otto-intel-mcp/adapter-index.js` (moved 2026-08-29; see that entry's header). */
2
+ export { ERC20_ALLOWANCE_ABI, ERC20_APPROVE_ABI, readAllowance } from './vendor/otto-intel-mcp/adapter-index.js';
3
+ //# sourceMappingURL=erc20.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * otto-execute — the USER'S side of Model B.
3
+ *
4
+ * Otto's hosted MCP constructs transactions prepare-only (`otto_prepare_*`, unsigned envelopes with
5
+ * Otto's fee attribution baked in). This package is what a user's own signer runs BEFORE and WHILE
6
+ * signing one: verify (integrity → intent → policy → attribution), then sign/submit with the user's
7
+ * key (BYO/EOA: the user signs and submits) or under a delegation the user minted (CDP-delegated:
8
+ * user-authorized, Otto-submitted, Otto-governed, Otto-revocable). Otto never holds user keys or funds.
9
+ */
10
+ export { ADAPTER_REFUSAL_CODES, AdapterRefusal, isAdapterRefusal, isAdapterRefusalCode, type AdapterRefusalCode, } from './refusal.js';
11
+ export { COLLECT_TOKEN_FEES_SELECTOR, GENERIC_SWAP_SELECTORS, decodeCollectTokenFees, decodeGenericSwap, encodeCollectTokenFees, encodeGenericSwap, type DecodedCollectTokenFees, type DecodedGenericSwap, type FeeDistribution, type SwapLeg, } from './lifi-decode.js';
12
+ export { VERIFY_CHECK_IDS, assertVerifiedPlanIntact, defaultAdapterPolicy, isVerifierOutput, verifySwapEnvelope, type AdapterPolicy, type AttributionPolicy, type SigningCapability, type SwapIntent, type VerifiedCheck, type VerifiedStep, type VerifiedSwap, type VerifyExpectations, } from './verify.js';
13
+ export { waitForSealedReceipt, type SealedClient, type SealedWaitOptions } from './chain.js';
14
+ export { EOA_CAPABILITIES, EoaSigner, PlanHaltedError, type DryRunSignedStep, type SentStep, type SignedX402Payment, } from './eoa-signer.js';
15
+ export { BASE_USDC, OTTO_X402_PAY_TO, X402_REFUSAL_CODES, X402_TABLE, X402_TABLE_VERSION, X402Refusal, canonicalResourceUrl, requirementMatchesRow, validateX402Challenge, type PaymentRequirementLike, type ValidatedX402Challenge, type X402ExpectedRow, type X402RefusalCode, type X402Table, } from './x402-table.js';
16
+ export { FOREIGN_ADDRESS, FOREIGN_TOKEN, MUTATION_CASES, applyMutation, recommit, recommitCommitmentOnly, recommitPayloadOnly, type CatchingLayer, type MutationCase, type MutationContext, } from './mutations.js';
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * otto-execute — the USER'S side of Model B.
3
+ *
4
+ * Otto's hosted MCP constructs transactions prepare-only (`otto_prepare_*`, unsigned envelopes with
5
+ * Otto's fee attribution baked in). This package is what a user's own signer runs BEFORE and WHILE
6
+ * signing one: verify (integrity → intent → policy → attribution), then sign/submit with the user's
7
+ * key (BYO/EOA: the user signs and submits) or under a delegation the user minted (CDP-delegated:
8
+ * user-authorized, Otto-submitted, Otto-governed, Otto-revocable). Otto never holds user keys or funds.
9
+ */
10
+ export { ADAPTER_REFUSAL_CODES, AdapterRefusal, isAdapterRefusal, isAdapterRefusalCode, } from './refusal.js';
11
+ export { COLLECT_TOKEN_FEES_SELECTOR, GENERIC_SWAP_SELECTORS, decodeCollectTokenFees, decodeGenericSwap, encodeCollectTokenFees, encodeGenericSwap, } from './lifi-decode.js';
12
+ export { VERIFY_CHECK_IDS, assertVerifiedPlanIntact, defaultAdapterPolicy, isVerifierOutput, verifySwapEnvelope, } from './verify.js';
13
+ export { waitForSealedReceipt } from './chain.js';
14
+ export { EOA_CAPABILITIES, EoaSigner, PlanHaltedError, } from './eoa-signer.js';
15
+ // The CDP-delegated signer lives on its own entry, `otto-execute/cdp`: only its `openCdpRig` needs the
16
+ // optional `@coinbase/cdp-sdk` peer (loaded lazily); this root never references the SDK at all.
17
+ export { BASE_USDC, OTTO_X402_PAY_TO, X402_REFUSAL_CODES, X402_TABLE, X402_TABLE_VERSION, X402Refusal, canonicalResourceUrl, requirementMatchesRow, validateX402Challenge, } from './x402-table.js';
18
+ export { FOREIGN_ADDRESS, FOREIGN_TOKEN, MUTATION_CASES, applyMutation, recommit, recommitCommitmentOnly, recommitPayloadOnly, } from './mutations.js';
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,2 @@
1
+ /** lifi-decode.ts — re-exported from `./vendor/otto-intel-mcp/adapter-index.js` (moved 2026-08-29; see that entry's header). */
2
+ export { COLLECT_TOKEN_FEES_SELECTOR, GENERIC_SWAP_SELECTORS, decodeCollectTokenFees, decodeGenericSwap, encodeCollectTokenFees, encodeGenericSwap, type DecodedCollectTokenFees, type DecodedGenericSwap, type FeeDistribution, type SwapLeg, } from './vendor/otto-intel-mcp/adapter-index.js';
@@ -0,0 +1,3 @@
1
+ /** lifi-decode.ts — re-exported from `./vendor/otto-intel-mcp/adapter-index.js` (moved 2026-08-29; see that entry's header). */
2
+ export { COLLECT_TOKEN_FEES_SELECTOR, GENERIC_SWAP_SELECTORS, decodeCollectTokenFees, decodeGenericSwap, encodeCollectTokenFees, encodeGenericSwap, } from './vendor/otto-intel-mcp/adapter-index.js';
3
+ //# sourceMappingURL=lifi-decode.js.map
@@ -0,0 +1,55 @@
1
+ /**
2
+ * mutations.ts — the consistency matrix (mutation cases the adapter MUST refuse).
3
+ *
4
+ * Each entry derives an INCONSISTENT variant of a VALID artifact and names the exact refusal the
5
+ * adapter must produce. Used twice: by the test suite (every refusal must redden — delete a check
6
+ * in verify.ts and its row fails) and by the sitting's fence section (the BYO "policy engine" is the
7
+ * adapter itself; these rows are its out-of-policy inputs, graded BLOCKED / LEAKED by the ladder).
8
+ * Same vocabulary as the constructor's own mutation proofs (`test/*-mutation-proof.ts`).
9
+ *
10
+ * Two mutation models, deliberately both:
11
+ * - `recommit: false` — bytes change, digests are left stale. The integrity layer catches it.
12
+ * - `recommit: true` — payload + commitment digests are recomputed (they are unkeyed keccak over
13
+ * public content, so any party holding the artifact can). Only the adapter's INTENT and POLICY
14
+ * layers can catch these. A verifier that stopped at digests would grade every one of them LEAKED.
15
+ *
16
+ * Every row names the layer expected to catch it. Rows caught by the shared envelope schema are kept
17
+ * (they document the contract) but are NOT evidence for a verifier-local check; the verifier-local
18
+ * checks each have their own row(s) below.
19
+ *
20
+ * Deliberately NOT a row: an internal DEX leg re-pointed at a foreign contract with the receiver and
21
+ * minimum output intact. The adapter does not hold an allowlist of every DEX; that leg is bounded
22
+ * ON-CHAIN by the router's `_receiver` + `_minAmount` enforcement (GenericSwapV3 reverts unless the
23
+ * receiver gets at least the minimum), which is why the intent floor must be positive and the
24
+ * caller's own (verify.ts layer 2).
25
+ */
26
+ import { type Address } from 'viem';
27
+ import type { AdapterRefusalCode } from './refusal.js';
28
+ import { type ExecutionEnvelope } from './vendor/otto-intel-mcp/execution-index.js';
29
+ /** A foreign address no reviewed table names; the mutation target for every substitution row. */
30
+ export declare const FOREIGN_ADDRESS: `0x${string}`;
31
+ /** A foreign ERC20 no intent names (Base USDbC), for token substitutions. */
32
+ export declare const FOREIGN_TOKEN: `0x${string}`;
33
+ /** Recompute both digests with the constructor's public canonicalization, as any holder of the artifact could. */
34
+ export declare function recommit(artifact: ExecutionEnvelope): ExecutionEnvelope;
35
+ /** Which adapter layer is expected to catch the mutation (recorded in evidence). */
36
+ export type CatchingLayer = 'schema' | 'integrity' | 'intent' | 'policy';
37
+ export interface MutationCase {
38
+ readonly id: string;
39
+ readonly what: string;
40
+ readonly recommit: boolean;
41
+ readonly expect: AdapterRefusalCode;
42
+ readonly layer: CatchingLayer;
43
+ readonly mutate: (artifact: ExecutionEnvelope, context: MutationContext) => ExecutionEnvelope;
44
+ }
45
+ export interface MutationContext {
46
+ readonly forwarders: readonly Address[];
47
+ readonly ottoRecipient: Address;
48
+ readonly nowMs: number;
49
+ }
50
+ export declare const MUTATION_CASES: readonly MutationCase[];
51
+ /** Recompute ONLY the payload digest (commitment left stale) — isolates the commitment check. */
52
+ export declare function recommitPayloadOnly(artifact: ExecutionEnvelope): ExecutionEnvelope;
53
+ /** Recompute ONLY the whole-envelope commitment (payload digest left stale) — isolates the payload-digest check. */
54
+ export declare function recommitCommitmentOnly(artifact: ExecutionEnvelope): ExecutionEnvelope;
55
+ export declare function applyMutation(mutation: MutationCase, artifact: ExecutionEnvelope, context: MutationContext): ExecutionEnvelope;