otto-intel-mcp 0.1.2 → 0.1.3
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/README.md +4 -2
- package/dist/adapter/cdp-signer.d.ts +44 -5
- package/dist/adapter/cdp-signer.js +168 -18
- package/dist/adapter/evm-call-failure.d.ts +29 -0
- package/dist/adapter/evm-call-failure.js +109 -0
- package/dist/adapter/refusal.d.ts +1 -1
- package/dist/adapter/refusal.js +13 -0
- package/dist/adapter/route-simulator.d.ts +91 -0
- package/dist/adapter/route-simulator.js +143 -0
- package/dist/executable-equities.d.ts +55 -0
- package/dist/executable-equities.js +116 -0
- package/dist/executable-yield-markets.d.ts +261 -0
- package/dist/executable-yield-markets.js +154 -0
- package/dist/execution-delegated-definition.d.ts +1 -0
- package/dist/execution-delegated-definition.js +13 -2
- package/dist/execution-delegation-admin-definition.d.ts +126 -5
- package/dist/execution-delegation-admin-definition.js +154 -5
- package/dist/execution-delegation-admin.d.ts +48 -2
- package/dist/execution-delegation-admin.js +314 -12
- package/dist/execution-delegation-cap-store.d.ts +369 -0
- package/dist/execution-delegation-cap-store.js +1083 -0
- package/dist/execution-delegation-policy.d.ts +153 -6
- package/dist/execution-delegation-policy.js +260 -16
- package/dist/execution-delegation.d.ts +35 -0
- package/dist/execution-delegation.js +224 -9
- package/dist/execution-errors.d.ts +1 -1
- package/dist/execution-errors.js +98 -0
- package/dist/execution-index.d.ts +3 -2
- package/dist/execution-index.js +3 -2
- package/dist/execution-registration.d.ts +51 -2
- package/dist/execution-registration.js +45 -2
- package/dist/execution-tools.d.ts +44 -1
- package/dist/execution-tools.js +157 -13
- package/dist/lifi-execution-client.d.ts +6 -0
- package/dist/lifi-execution-client.js +3 -0
- package/dist/stock-buy-definition.d.ts +338 -0
- package/dist/stock-buy-definition.js +127 -0
- package/dist/stock-buy-oracle.d.ts +8 -0
- package/dist/stock-buy-oracle.js +22 -0
- package/dist/stock-buy.d.ts +17 -0
- package/dist/stock-buy.js +164 -0
- package/dist/tool-definitions.js +1 -1
- package/dist/yield-deposit-definition.d.ts +323 -0
- package/dist/yield-deposit-definition.js +121 -0
- package/dist/yield-deposit.d.ts +16 -0
- package/dist/yield-deposit.js +141 -0
- package/dist/yield-withdraw-definition.d.ts +306 -0
- package/dist/yield-withdraw-definition.js +88 -0
- package/dist/yield-withdraw.d.ts +21 -0
- package/dist/yield-withdraw.js +120 -0
- package/package.json +28 -4
|
@@ -31,6 +31,20 @@
|
|
|
31
31
|
*
|
|
32
32
|
* Nothing here logs the header, the secret, or a credential. Refusals are machine-readable codes; upstream
|
|
33
33
|
* error bodies are never relayed.
|
|
34
|
+
*
|
|
35
|
+
* ══════════════════════════════════════════════════════════════════════════════════════════════════
|
|
36
|
+
* 🔴 THE THREAT MODEL THE PER-SWAP CAP GATE (step 5b) ENFORCES (Founder ruling, 2026-09-03, verbatim)
|
|
37
|
+
* ══════════════════════════════════════════════════════════════════════════════════════════════════
|
|
38
|
+
* "The per-person delegation cap protects every delegated send that goes through Otto's page
|
|
39
|
+
* (/app/delegate) and Otto's CLI (otto-execute delegate). A permission a person mints or revokes
|
|
40
|
+
* directly against Coinbase with their own login, outside Otto's page and CLI, is OUT OF SCOPE: Otto
|
|
41
|
+
* cannot distinguish it (CDP returns no grant identity, only an expiry) and does not defend against it;
|
|
42
|
+
* the CDP project-scope $5,000 ceiling remains the hard backstop for every permission. The delegated
|
|
43
|
+
* agent never holds the person's login."
|
|
44
|
+
*
|
|
45
|
+
* Note the direction that holds even out of the model: a grant Otto has no CONFIRMED record for
|
|
46
|
+
* authorises NOTHING here — it is refused by name, never admitted at the shared ceiling. An
|
|
47
|
+
* out-of-model actor can therefore shorten what Otto may do for them, never widen it.
|
|
34
48
|
*/
|
|
35
49
|
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
36
50
|
import { getAddress } from 'viem';
|
|
@@ -38,7 +52,9 @@ import { CDP_CAPABILITIES, CdpDelegatedSigner, DelegatedPlanHaltedError, createC
|
|
|
38
52
|
import { isAdapterRefusal } from './adapter/refusal.js';
|
|
39
53
|
import { defaultAdapterPolicy, verifySwapEnvelope } from './adapter/verify.js';
|
|
40
54
|
import { DELEGATION_AUTH_HEADER, SUBMIT_UNDER_DELEGATION_INPUT_SCHEMA, } from './execution-delegated-definition.js';
|
|
41
|
-
import {
|
|
55
|
+
import { MODEL_B_CAP_RULESET_VERSION } from './execution-delegation-cap-store.js';
|
|
56
|
+
import { assertWithinModelBFence, assertWithinUserPerSwapCap, MODEL_B_CHAIN_ID, MODEL_B_DELEGATION_EXPIRY_MAX_DAYS, MODEL_B_PER_SWAP_CAP_USD, MODEL_B_POLICY_VERSION, assertWithdrawWithinModelBFence, modelBWithdrawToken, MODEL_B_INPUT_TOKENS_V1, readBackModelBFence, } from './execution-delegation-policy.js';
|
|
57
|
+
import { createDelegationCapStore, resolveDelegationMintEnabled, } from './execution-delegation-cap-store.js';
|
|
42
58
|
import { EXECUTION_REFUSAL_CODES, ExecutionRefusal, isExecutionRefusal, refuse, } from './execution-errors.js';
|
|
43
59
|
import { artifactCommitmentDigest, canonicalJson, } from './execution-types.js';
|
|
44
60
|
import { DELEGATED_SUBMISSION_GRACE_MS, preparedHere } from './prepared-artifacts.js';
|
|
@@ -357,15 +373,32 @@ function verifiedResult(input, user, verified, sent, fence, expiresAt) {
|
|
|
357
373
|
block_number: step.block_number,
|
|
358
374
|
gas_used: step.gas_used,
|
|
359
375
|
})),
|
|
376
|
+
/**
|
|
377
|
+
* 🔴 TWO NUMBERS, BECAUSE THERE ARE TWO FACTS. `per_swap_cap_usd` is the ENGINE's — the constant
|
|
378
|
+
* compiled into the one project-scope policy, the same for every delegated user, and the only one
|
|
379
|
+
* Coinbase enforces. `user_cap_usd` is what THIS person chose when they minted, enforced by this
|
|
380
|
+
* server alone. Reporting only the second would describe the CDP fence as something it is not;
|
|
381
|
+
* reporting only the first would hide the number that actually bounded this send.
|
|
382
|
+
*/
|
|
360
383
|
fence: Object.freeze({
|
|
361
384
|
policy_id: fence.id,
|
|
362
|
-
policy_name:
|
|
385
|
+
policy_name: fence.name,
|
|
363
386
|
rules_digest: fence.rulesDigest,
|
|
364
387
|
per_swap_cap_usd: MODEL_B_PER_SWAP_CAP_USD,
|
|
388
|
+
user_cap_usd: fence.userCapUsd,
|
|
365
389
|
}),
|
|
366
390
|
delegation: Object.freeze({ expires_at: expiresAt }),
|
|
367
391
|
verification: Object.freeze({
|
|
368
|
-
|
|
392
|
+
/**
|
|
393
|
+
* The verifier's checks, plus the ROUTE the constructor recorded in the envelope (`route_tool:<lifi
|
|
394
|
+
* tool>` and `route_simulated:<true|false>` assertion ids), so the receipt says which venue executed.
|
|
395
|
+
* Carried as check strings because the receipt's wire shape is validated `.strict()` by the shipped
|
|
396
|
+
* dApp until cut #3 — a new field would break the page that is live today.
|
|
397
|
+
*/
|
|
398
|
+
checks: [
|
|
399
|
+
...verified.checks.map((check) => check.id),
|
|
400
|
+
...input.envelope.assertions.map((a) => a.id).filter((id) => id.startsWith('route_')),
|
|
401
|
+
],
|
|
369
402
|
gross: verified.gross.toString(),
|
|
370
403
|
min_amount_out: verified.minAmountOut.toString(),
|
|
371
404
|
otto_fee_atomic: verified.ottoFee.toString(),
|
|
@@ -427,13 +460,19 @@ export async function submitUnderDelegation(runtime, rawInput, caller, _signal)
|
|
|
427
460
|
const fence = readBackModelBFence(await services.rig.listProjectPolicies());
|
|
428
461
|
if (!fence.present)
|
|
429
462
|
return refuse('DELEGATION_FENCE_ABSENT', `the Model-B project policy is not in force (${fence.reason}): ${fence.detail}`);
|
|
430
|
-
// 3. GRANT active past the envelope's own expiry; fail-closed once revocation was
|
|
431
|
-
if (revocationRequested(services.rig.rig, input.userId)) {
|
|
432
|
-
return refuse('DELEGATION_NOT_ACTIVE', 'revocation was requested for this end user on this server; nothing is submitted for them again');
|
|
433
|
-
}
|
|
463
|
+
// 3. GRANT active past the envelope's own expiry; fail-closed once revocation was requested for IT.
|
|
434
464
|
const grant = await services.rig.rig.readDelegation(input.userId);
|
|
435
465
|
if (!grant)
|
|
436
466
|
return refuse('DELEGATION_NOT_ACTIVE', 'no active delegation reads back for this end user');
|
|
467
|
+
/**
|
|
468
|
+
* 🔴 PER GRANT, NOT PER USER (gate finding, HIGH — round 3). The marker used to be a sticky set of
|
|
469
|
+
* user ids, so once revoke was offered in every screen state a Stop press on a screen with no grant
|
|
470
|
+
* bricked every later permission that person minted. It names the grant now, so a revoke stops
|
|
471
|
+
* exactly what it was aimed at.
|
|
472
|
+
*/
|
|
473
|
+
if (revocationRequested(services.rig.rig, input.userId, grant.expiresAt)) {
|
|
474
|
+
return refuse('DELEGATION_NOT_ACTIVE', 'revocation was requested for this delegation on this server; nothing is submitted under it');
|
|
475
|
+
}
|
|
437
476
|
const expiresAtMs = Date.parse(grant.expiresAt);
|
|
438
477
|
const validUntilMs = Date.parse(input.envelope.valid_until);
|
|
439
478
|
if (!Number.isFinite(expiresAtMs) ||
|
|
@@ -500,10 +539,176 @@ export async function submitUnderDelegation(runtime, rawInput, caller, _signal)
|
|
|
500
539
|
throw refusal;
|
|
501
540
|
throw error;
|
|
502
541
|
}
|
|
503
|
-
|
|
542
|
+
/**
|
|
543
|
+
* 🔴 v2 — TWO SHAPES THROUGH ONE GATE. A delegated plan spends either an allow-listed INPUT token (a swap,
|
|
544
|
+
* or a Yield Copilot DEPOSIT, which is a swap into vault shares) — capped in USD by the engine fence and
|
|
545
|
+
* by the person's own recorded cap — or a registry vault SHARE token (a Yield Copilot WITHDRAW: shares →
|
|
546
|
+
* USDC, back to the signing account). A withdraw returns the person's own assets to the person and never
|
|
547
|
+
* increases exposure, so the person's per-swap cap does not apply to it: the cap checks below run with a
|
|
548
|
+
* gross of ZERO (which still validates the recorded cap's sanity). What bounds a withdraw is the engine's
|
|
549
|
+
* share cap (`assertWithdrawWithinModelBFence`) and the verifier's receiver pin, and it is admitted ONLY
|
|
550
|
+
* under fence v2 and ONLY for a permission minted under cap ruleset v2 (the person consented to withdraws).
|
|
551
|
+
*/
|
|
552
|
+
const inputTokens = services.fenceBound?.inputTokens ?? MODEL_B_INPUT_TOKENS_V1;
|
|
553
|
+
// A withdraw is recognised by its INPUT being a registry share token; anything else takes the swap path
|
|
554
|
+
// (and a token outside the allow-list is refused there, by the original message).
|
|
555
|
+
const isWithdraw = modelBWithdrawToken(verified.token) !== undefined;
|
|
556
|
+
let capGross;
|
|
557
|
+
let capToken;
|
|
558
|
+
if (isWithdraw) {
|
|
559
|
+
if (fence.version < 2) {
|
|
560
|
+
return refuse('DELEGATION_OUTSIDE_FENCE', `a delegated vault withdraw needs fence v${MODEL_B_POLICY_VERSION}; the project fence is v${fence.version} — nothing is submitted`);
|
|
561
|
+
}
|
|
562
|
+
assertWithdrawWithinModelBFence({
|
|
563
|
+
token: verified.token,
|
|
564
|
+
spender: verified.spender,
|
|
565
|
+
gross: verified.gross,
|
|
566
|
+
outputToken: verified.outputToken,
|
|
567
|
+
});
|
|
568
|
+
capGross = 0n;
|
|
569
|
+
capToken = MODEL_B_INPUT_TOKENS_V1[0];
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
const bound = assertWithinModelBFence(verified, services.fenceBound?.perSwapCapUsd, inputTokens);
|
|
573
|
+
capGross = verified.gross;
|
|
574
|
+
capToken = bound.token;
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* 5b. THE SEND GATE (Founder word `delegate cap choice`; ROUND 4, built to the codex gate's own
|
|
578
|
+
* prescription after it NO-GO'd rounds 1–3).
|
|
579
|
+
*
|
|
580
|
+
* ══════════════════════════════════════════════════════════════════════════════════════════════
|
|
581
|
+
* 🔴 WHAT THIS REFUSES, AND WHY IT IS A CONFIRMED RECORD RATHER THAN A ROW
|
|
582
|
+
* ══════════════════════════════════════════════════════════════════════════════════════════════
|
|
583
|
+
* Round 3 read a durable row keyed by `(userId, grant.expiresAt)`. The gate's two CRITICALs:
|
|
584
|
+
*
|
|
585
|
+
* "The durable row still authenticates an expiry, not a grant … it does not require a successful
|
|
586
|
+
* confirmation, the reservation's mintKey, or an active-grant pointer."
|
|
587
|
+
* "Confirmation is not an enforcement gate. Reserve writes the complete enforceable capability;
|
|
588
|
+
* confirm writes no state at all."
|
|
589
|
+
*
|
|
590
|
+
* So a reservation was a bearer capability: anyone who could ask CDP for a grant at that instant —
|
|
591
|
+
* the agent CLI, an SDK client, a second tab — inherited the cap without confirming.
|
|
592
|
+
*
|
|
593
|
+
* The gate now asks for the CONFIRMED-ACTIVE record and nothing else. A reserved, superseded,
|
|
594
|
+
* revoked or expired record is not returned by `readActive` at all, so:
|
|
595
|
+
*
|
|
596
|
+
* · a grant minted at a reserved instant WITHOUT confirming is refused `DELEGATION_CAP_UNCONFIRMED`;
|
|
597
|
+
* · a grant that is not the one the confirmed record names — an old expiry reused, a permission
|
|
598
|
+
* minted elsewhere — is refused `DELEGATION_CAP_MISMATCH`;
|
|
599
|
+
* · a record from a NEWER cap ruleset than this worker implements is refused
|
|
600
|
+
* `DELEGATION_CAP_RULESET_STALE`, which is what stops a rolling deploy's older binary executing
|
|
601
|
+
* a new permission at the shared ceiling.
|
|
602
|
+
*
|
|
603
|
+
* None of them fall back to the engine's $5,000: "missing means the ceiling" would turn every one of
|
|
604
|
+
* these into a silent widening of somebody's permission.
|
|
605
|
+
*
|
|
606
|
+
* 🔴 AND IT IS RE-DERIVED FROM CURRENT DURABLE STATE ON EVERY READ, never from a boot-time map, so a
|
|
607
|
+
* process that missed another's confirmation still sees it — and a store that cannot be read THROWS
|
|
608
|
+
* rather than answering "nothing is confirmed", because those two have opposite consequences.
|
|
609
|
+
*/
|
|
610
|
+
const readActiveCap = async () => {
|
|
611
|
+
if (!services.capStore.available) {
|
|
612
|
+
return refuse('DELEGATION_CAP_STORE_UNAVAILABLE', `the per-swap cap for this delegation cannot be read, so what the user agreed to is unknown; nothing is submitted (${services.capStore.unavailableReason ?? 'store unavailable'})`);
|
|
613
|
+
}
|
|
614
|
+
let active;
|
|
615
|
+
try {
|
|
616
|
+
active = await services.capStore.readActive(input.userId, runtime.nowMs());
|
|
617
|
+
}
|
|
618
|
+
catch {
|
|
619
|
+
return refuse('DELEGATION_CAP_STORE_UNAVAILABLE', 'the per-swap cap for this delegation could not be read, so what the user agreed to is unknown; nothing is submitted');
|
|
620
|
+
}
|
|
621
|
+
if (!active) {
|
|
622
|
+
return refuse('DELEGATION_CAP_UNCONFIRMED', 'no confirmed permission stands for this end user on this server, so what they agreed to is unknown; nothing is submitted — mint the permission again on the delegation page');
|
|
623
|
+
}
|
|
624
|
+
if (active.requiredVersion > MODEL_B_CAP_RULESET_VERSION) {
|
|
625
|
+
return refuse('DELEGATION_CAP_RULESET_STALE', `this permission requires cap ruleset v${active.requiredVersion} and this server implements v${MODEL_B_CAP_RULESET_VERSION}; nothing is submitted until the newer build is serving`);
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* 🔴 THE CONFIRMED RECORD MUST NAME THE GRANT THAT IS ACTUALLY LIVE. Without this the record would
|
|
629
|
+
* be a standing authorisation for whatever grant the user happens to hold, which is the round-3
|
|
630
|
+
* defect one layer up.
|
|
631
|
+
*/
|
|
632
|
+
if (Date.parse(active.expiresAt) !== expiresAtMs) {
|
|
633
|
+
return refuse('DELEGATION_CAP_MISMATCH', 'the delegation that reads back is not the one this permission was confirmed for; nothing is submitted — mint the permission again on the delegation page');
|
|
634
|
+
}
|
|
635
|
+
return {
|
|
636
|
+
capUsd: active.perSwapCapUsd,
|
|
637
|
+
expiresAt: active.expiresAt,
|
|
638
|
+
requiredVersion: active.requiredVersion,
|
|
639
|
+
};
|
|
640
|
+
};
|
|
641
|
+
const active = await readActiveCap();
|
|
642
|
+
const userCapUsd = active.capUsd;
|
|
643
|
+
if (isWithdraw && active.requiredVersion < 2) {
|
|
644
|
+
return refuse('DELEGATION_WITHDRAW_UNCONSENTED', `this permission was minted under cap ruleset v${active.requiredVersion}, before vault withdraws existed; nothing is submitted — mint the permission again on the delegation page`);
|
|
645
|
+
}
|
|
646
|
+
assertWithinUserPerSwapCap(capGross, userCapUsd, capToken);
|
|
647
|
+
/**
|
|
648
|
+
* 🔴 AND IT IS RE-CHECKED BEFORE EVERY STEP, AND AGAIN AFTER THE TRANSACTION IS BUILT.
|
|
649
|
+
*
|
|
650
|
+
* The plan is three transactions. `beforeEachStep` re-reads the grant and the confirmed record
|
|
651
|
+
* between them, which catches a revoke-and-re-mint from another tab. The gate found the remaining
|
|
652
|
+
* window: "the final asynchronous grant/cap check still happens before transaction construction …
|
|
653
|
+
* only a synchronous abort check precedes raw submission. A revoke/re-mint to a narrower cap during
|
|
654
|
+
* transaction building therefore escapes the last authoritative check."
|
|
655
|
+
*
|
|
656
|
+
* So the async re-read stores what it saw, and a SYNCHRONOUS assertion over that value runs after
|
|
657
|
+
* the nonce/fee/gas round trips and immediately before the hand-off. It cannot await — awaiting is
|
|
658
|
+
* what opened the window — so what it closes is the interval between the last read and the send,
|
|
659
|
+
* using the freshest value that exists. The irreducible residual is the network round trip itself,
|
|
660
|
+
* which no vendor CAS is offered to close.
|
|
661
|
+
*/
|
|
662
|
+
let lastSeenCapUsd = userCapUsd;
|
|
663
|
+
let lastSeenRequiredVersion = active.requiredVersion;
|
|
664
|
+
let lastSeenGrantOk = true;
|
|
665
|
+
/** Re-read the grant and the confirmed record; record what was seen. Used by both hooks below. */
|
|
666
|
+
const refreshSeen = async () => {
|
|
667
|
+
const current = await services.rig.rig.readDelegation(input.userId);
|
|
668
|
+
lastSeenGrantOk = current !== undefined && Date.parse(current.expiresAt) === expiresAtMs;
|
|
669
|
+
if (!lastSeenGrantOk)
|
|
670
|
+
return;
|
|
671
|
+
const seen = await readActiveCap();
|
|
672
|
+
lastSeenCapUsd = seen.capUsd;
|
|
673
|
+
lastSeenRequiredVersion = seen.requiredVersion;
|
|
674
|
+
};
|
|
675
|
+
/** A withdraw's consent is re-asserted wherever the cap is (codex gate r1: true at all three points). */
|
|
676
|
+
const assertWithdrawConsent = () => {
|
|
677
|
+
if (isWithdraw && lastSeenRequiredVersion < 2) {
|
|
678
|
+
return refuse('DELEGATION_WITHDRAW_UNCONSENTED', 'the confirmed permission changed to one minted before vault withdraws existed; the remaining steps are not submitted');
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
/** Between steps: the plan must still be admissible before the next one is even built. */
|
|
682
|
+
const beforeEachStep = async () => {
|
|
683
|
+
await refreshSeen();
|
|
684
|
+
if (!lastSeenGrantOk) {
|
|
685
|
+
return refuse('DELEGATION_NOT_ACTIVE', 'the delegation changed while the plan was in flight; the remaining steps are not submitted');
|
|
686
|
+
}
|
|
687
|
+
assertWithdrawConsent();
|
|
688
|
+
assertWithinUserPerSwapCap(capGross, lastSeenCapUsd, capToken);
|
|
689
|
+
};
|
|
690
|
+
/**
|
|
691
|
+
* 🔴 AND AGAIN AFTER THE TRANSACTION IS BUILT — the window the gate named. The nonce, fee and gas
|
|
692
|
+
* round trips take real time, and a revoke or a narrower re-mint landing inside them was invisible
|
|
693
|
+
* to the check between steps. This one runs after construction; the synchronous assertion below then
|
|
694
|
+
* acts on what it found, with nothing awaited between it and the hand-off.
|
|
695
|
+
*/
|
|
696
|
+
const afterBuild = refreshSeen;
|
|
697
|
+
const beforeSubmit = () => {
|
|
698
|
+
if (!lastSeenGrantOk) {
|
|
699
|
+
return refuse('DELEGATION_NOT_ACTIVE', 'the delegation changed while this transaction was being built; it is not submitted');
|
|
700
|
+
}
|
|
701
|
+
assertWithdrawConsent();
|
|
702
|
+
assertWithinUserPerSwapCap(capGross, lastSeenCapUsd, capToken);
|
|
703
|
+
};
|
|
504
704
|
// 6. SEND.
|
|
505
705
|
const signer = new CdpDelegatedSigner(services.rig.rig, user, {
|
|
506
706
|
sealedWait: services.sealedWait,
|
|
707
|
+
beforeEachStep,
|
|
708
|
+
afterBuild,
|
|
709
|
+
beforeSubmit,
|
|
710
|
+
// The grant this plan is bound to: the revocation marker is per grant, so the signer names one.
|
|
711
|
+
grantExpiresAt: grant.expiresAt,
|
|
507
712
|
});
|
|
508
713
|
let sent;
|
|
509
714
|
try {
|
|
@@ -516,7 +721,7 @@ export async function submitUnderDelegation(runtime, rawInput, caller, _signal)
|
|
|
516
721
|
submission.state = { kind: 'halted', refusal };
|
|
517
722
|
throw refusal;
|
|
518
723
|
}
|
|
519
|
-
const result = verifiedResult(input, user, verified, sent, fence, grant.expiresAt);
|
|
724
|
+
const result = verifiedResult(input, user, verified, sent, { id: fence.id, rulesDigest: fence.rulesDigest, userCapUsd, name: fence.name }, grant.expiresAt);
|
|
520
725
|
submission.state = { kind: 'sent', result };
|
|
521
726
|
return result;
|
|
522
727
|
}
|
|
@@ -576,7 +781,17 @@ export async function openDelegationServicesFromEnv(env) {
|
|
|
576
781
|
}
|
|
577
782
|
return Object.freeze({
|
|
578
783
|
rig: createDelegationRig(cdp, pub),
|
|
784
|
+
pub,
|
|
579
785
|
serverSecret: env.serverSecret,
|
|
786
|
+
// 🔴 An unconfigured store does NOT stop the boot — it opens UNAVAILABLE, and every delegated send
|
|
787
|
+
// then refuses `DELEGATION_CAP_STORE_UNAVAILABLE` by name. Refusing to boot would also take down the
|
|
788
|
+
// read-only tools (status, revoke, the public fence promise) and, in particular, the REVOKE that lets
|
|
789
|
+
// a person withdraw authority — the one control that must never be collateral damage.
|
|
790
|
+
capStore: await createDelegationCapStore(),
|
|
791
|
+
// Default FALSE in code: an operator who never sets the variable gets the paused surface, never an
|
|
792
|
+
// unfenced first rollout. Read once at boot, so a mid-life environment change needs a redeploy —
|
|
793
|
+
// which is exactly what the rollout runbook does.
|
|
794
|
+
mintEnabled: resolveDelegationMintEnabled(),
|
|
580
795
|
...(env.publicProjectId ? { publicProjectId: env.publicProjectId } : {}),
|
|
581
796
|
});
|
|
582
797
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const EXECUTION_REFUSAL_CODES: readonly ["ATTRIBUTION_MISSING", "LIFI_FEE_CAP_EXCEEDED", "ACCOUNT_BINDING_MISMATCH", "DIGEST_MISMATCH", "ARTIFACT_INVALID", "STALE_ARTIFACT", "EXECUTION_RATE_LIMITED", "EXECUTION_CONCURRENCY_FULL", "UNSUPPORTED_ACCOUNT_PROFILE", "LIFI_QUOTE_FAILED", "LIFI_QUOTE_INVALID", "UNSUPPORTED_LIFI_ROUTE", "POLYMARKET_METADATA_FAILED", "POLYMARKET_METADATA_INVALID", "UNSUPPORTED_POLYMARKET_MARKET", "POLYMARKET_ORDER_INVALID", "POLYMARKET_BUILDER_FEE_UNRULED", "HYPERLIQUID_META_FAILED", "HYPERLIQUID_META_INVALID", "UNSUPPORTED_HYPERLIQUID_MARKET", "HYPERLIQUID_ORDER_INVALID", "DELEGATION_UNAVAILABLE", "DELEGATION_UNAUTHORIZED", "DELEGATION_INPUT_INVALID", "DELEGATION_BUSY", "DELEGATION_ARTIFACT_IN_FLIGHT", "ARTIFACT_NOT_PREPARED_HERE", "PREPARED_ARTIFACT_CAPACITY", "ARTIFACT_ID_COLLISION", "DELEGATION_REPLAY_MISMATCH", "DELEGATION_FENCE_ABSENT", "DELEGATION_FENCE_FOREIGN", "DELEGATION_FENCE_AMBIGUOUS", "DELEGATION_FENCE_NEWER", "DELEGATION_TOKEN_INVALID", "DELEGATION_REVOKE_NOT_CONFIRMED", "DELEGATION_NOT_ACTIVE", "DELEGATION_EXPIRY_EXCEEDS_BOUND", "DELEGATION_USER_UNKNOWN", "DELEGATION_OUTSIDE_FENCE", "DELEGATED_PLAN_REFUSED", "DELEGATED_PLAN_HALTED", "DELEGATION_UPSTREAM_FAILED"];
|
|
1
|
+
export declare const EXECUTION_REFUSAL_CODES: readonly ["ATTRIBUTION_MISSING", "LIFI_FEE_CAP_EXCEEDED", "ACCOUNT_BINDING_MISMATCH", "DIGEST_MISMATCH", "ARTIFACT_INVALID", "STALE_ARTIFACT", "EXECUTION_RATE_LIMITED", "EXECUTION_CONCURRENCY_FULL", "UNSUPPORTED_ACCOUNT_PROFILE", "LIFI_QUOTE_FAILED", "LIFI_QUOTE_INVALID", "UNSUPPORTED_LIFI_ROUTE", "POLYMARKET_METADATA_FAILED", "POLYMARKET_METADATA_INVALID", "UNSUPPORTED_POLYMARKET_MARKET", "POLYMARKET_ORDER_INVALID", "POLYMARKET_BUILDER_FEE_UNRULED", "HYPERLIQUID_META_FAILED", "HYPERLIQUID_META_INVALID", "UNSUPPORTED_HYPERLIQUID_MARKET", "HYPERLIQUID_ORDER_INVALID", "DELEGATION_UNAVAILABLE", "DELEGATION_UNAUTHORIZED", "DELEGATION_INPUT_INVALID", "DELEGATION_BUSY", "DELEGATION_ARTIFACT_IN_FLIGHT", "ARTIFACT_NOT_PREPARED_HERE", "PREPARED_ARTIFACT_CAPACITY", "ARTIFACT_ID_COLLISION", "DELEGATION_REPLAY_MISMATCH", "DELEGATION_FENCE_ABSENT", "DELEGATION_FENCE_FOREIGN", "DELEGATION_FENCE_AMBIGUOUS", "DELEGATION_FENCE_NEWER", "DELEGATION_TOKEN_INVALID", "DELEGATION_REVOKE_NOT_CONFIRMED", "DELEGATION_NOT_ACTIVE", "DELEGATION_EXPIRY_EXCEEDS_BOUND", "DELEGATION_USER_UNKNOWN", "DELEGATION_OUTSIDE_FENCE", "DELEGATION_ABOVE_USER_CAP", "DELEGATION_CAP_UNRECORDED", "DELEGATION_CAP_GRANT_CHANGED", "DELEGATION_CAP_EXPIRY_UNAVAILABLE", "DELEGATION_CAP_UNCONFIRMED", "DELEGATION_CAP_MISMATCH", "DELEGATION_CAP_RULESET_STALE", "DELEGATION_CAP_SUPERSEDED", "DELEGATION_CAP_STORE_UNAVAILABLE", "DELEGATION_MINT_PAUSED", "DELEGATION_CAP_CANCELLED", "DELEGATED_PLAN_REFUSED", "DELEGATED_PLAN_HALTED", "DELEGATION_UPSTREAM_FAILED", "YIELD_MARKET_UNKNOWN", "YIELD_REGISTRY_MISMATCH", "YIELD_PREVIEW_UNAVAILABLE", "YIELD_DEPOSIT_PARITY", "YIELD_WITHDRAW_PARITY", "DELEGATION_WITHDRAW_UNCONSENTED", "EQUITY_UNKNOWN", "EQUITY_REGISTRY_MISMATCH", "EQUITY_ORACLE_UNAVAILABLE", "EQUITY_ORACLE_FROZEN", "STOCK_BUY_PARITY"];
|
|
2
2
|
export type ExecutionRefusalCode = (typeof EXECUTION_REFUSAL_CODES)[number];
|
|
3
3
|
/** A fail-closed, machine-readable refusal. No upstream response bodies or secrets are retained. */
|
|
4
4
|
export declare class ExecutionRefusal extends Error {
|
package/dist/execution-errors.js
CHANGED
|
@@ -109,12 +109,110 @@ export const EXECUTION_REFUSAL_CODES = [
|
|
|
109
109
|
'DELEGATION_USER_UNKNOWN',
|
|
110
110
|
/** A verified plan the fence would refuse anyway: input token not allow-listed, spender not the reviewed diamond, gross over the per-swap cap. */
|
|
111
111
|
'DELEGATION_OUTSIDE_FENCE',
|
|
112
|
+
/**
|
|
113
|
+
* The plan is inside the ENGINE's fence but over the per-swap cap THIS USER chose when they minted.
|
|
114
|
+
* Deliberately distinct from `DELEGATION_OUTSIDE_FENCE`: that code means "Coinbase would refuse this
|
|
115
|
+
* too"; this one means "the engine would have signed it and Otto did not, because the person set a
|
|
116
|
+
* lower number." Conflating them would make the user's own choice indistinguishable from the shared
|
|
117
|
+
* ceiling, and no test could tell whether the chosen cap was ever consulted.
|
|
118
|
+
*/
|
|
119
|
+
'DELEGATION_ABOVE_USER_CAP',
|
|
120
|
+
/**
|
|
121
|
+
* The grant that reads back from CDP carries NO recorded per-swap cap on this server, so what the person
|
|
122
|
+
* agreed to is unknown. Fail-closed: nothing is submitted, and the remedy is a fresh mint (which records
|
|
123
|
+
* one). Never defaulted to the ceiling — a grant minted for $10 must not become a $5,000 grant because
|
|
124
|
+
* its record could not be found.
|
|
125
|
+
*/
|
|
126
|
+
'DELEGATION_CAP_UNRECORDED',
|
|
127
|
+
/**
|
|
128
|
+
* CONFIRM FAILED: the instant CDP reports for the live grant is not the instant this mint RESERVED.
|
|
129
|
+
*
|
|
130
|
+
* Round 3 makes `(userId, expiry)` a grant identity by construction — the server picks a whole second no
|
|
131
|
+
* durable row for that user occupies, writes the cap against it, and the page mints with exactly that
|
|
132
|
+
* second. This code is what happens when that construction does not hold: either another actor's grant
|
|
133
|
+
* is what reads back (replaced between the reservation and the mint), or CDP stored something other than
|
|
134
|
+
* what was asked for. Both mean the live permission is not the one this cap was chosen for, so nothing is
|
|
135
|
+
* blessed and the fresh grant is REVOKED rather than left standing with a cap it was never matched to.
|
|
136
|
+
*/
|
|
137
|
+
'DELEGATION_CAP_GRANT_CHANGED',
|
|
138
|
+
/**
|
|
139
|
+
* The offset ladder found no free whole-second instant at or before the requested expiry within its
|
|
140
|
+
* bound. Nothing is reserved and nothing is minted: handing out an instant a reservation already holds
|
|
141
|
+
* would put two permissions on one enforcement key, which is the collision this design removes.
|
|
142
|
+
*/
|
|
143
|
+
'DELEGATION_CAP_EXPIRY_UNAVAILABLE',
|
|
144
|
+
/**
|
|
145
|
+
* 🔴 THE SEND GATE (round 4). No CONFIRMED-ACTIVE record stands for this end user, so no permission
|
|
146
|
+
* has completed `reserved → confirmed_active` on this server. A reservation on its own authorises
|
|
147
|
+
* NOTHING: that is what stops a grant minted at a reserved instant by any client — the agent CLI, an
|
|
148
|
+
* SDK caller, a second tab — from inheriting a cap it never confirmed. Never defaulted to the ceiling.
|
|
149
|
+
*/
|
|
150
|
+
'DELEGATION_CAP_UNCONFIRMED',
|
|
151
|
+
/**
|
|
152
|
+
* A confirmed-active record stands, but the grant CDP reports right now is not the one it names. The
|
|
153
|
+
* live permission is a different one — replaced, or minted elsewhere — so nothing is submitted under
|
|
154
|
+
* it. Distinct from `DELEGATION_CAP_UNCONFIRMED`: that means nothing was ever confirmed; this means
|
|
155
|
+
* something was, and it is not what is live.
|
|
156
|
+
*/
|
|
157
|
+
'DELEGATION_CAP_MISMATCH',
|
|
158
|
+
/**
|
|
159
|
+
* The record requires a NEWER cap ruleset than this worker implements. A rolling deploy runs two
|
|
160
|
+
* binaries at once, and the Coinbase fence is identical on both — so an older worker that cannot
|
|
161
|
+
* honour a per-user cap would execute a new permission at the shared ceiling. It refuses instead.
|
|
162
|
+
*/
|
|
163
|
+
'DELEGATION_CAP_RULESET_STALE',
|
|
164
|
+
/**
|
|
165
|
+
* The reservation was confirmed once and a later confirmation replaced it. It is never revived: a
|
|
166
|
+
* superseded permission returning to active is a replaced cap coming back, and on a wider number that
|
|
167
|
+
* is the widening this unit exists to prevent.
|
|
168
|
+
*/
|
|
169
|
+
'DELEGATION_CAP_SUPERSEDED',
|
|
170
|
+
/**
|
|
171
|
+
* The per-swap cap store itself is unavailable — no durable path is configured, or it could not be read.
|
|
172
|
+
* Every delegated send refuses while that is true, because an unreadable store is indistinguishable from
|
|
173
|
+
* an empty one and the difference decides whether a user's cap is honoured.
|
|
174
|
+
*/
|
|
175
|
+
'DELEGATION_CAP_STORE_UNAVAILABLE',
|
|
176
|
+
/**
|
|
177
|
+
* This worker is not creating NEW permissions right now (`DELEGATION_MINT_ENABLED` is not set true) —
|
|
178
|
+
* the staged first deployment. It gates the RESERVE only: a permission that already stands keeps
|
|
179
|
+
* working under its own cap, an in-flight mint can still be confirmed, and revoke always works.
|
|
180
|
+
*/
|
|
181
|
+
'DELEGATION_MINT_PAUSED',
|
|
182
|
+
/**
|
|
183
|
+
* The person pressed Stop while this permission was being created, so the reservation it was minted
|
|
184
|
+
* against was cancelled. The fresh grant is revoked and nothing is confirmed for it. Never a retry:
|
|
185
|
+
* a Stop that leaves a permission behind is not a Stop.
|
|
186
|
+
*/
|
|
187
|
+
'DELEGATION_CAP_CANCELLED',
|
|
112
188
|
/** The user-side verifier refused the envelope with an adapter code the constructor does not share; the message carries `[CODE]`. */
|
|
113
189
|
'DELEGATED_PLAN_REFUSED',
|
|
114
190
|
/** The plan halted mid-way; the message reports the failed step and whether the allowance clear read back zero. */
|
|
115
191
|
'DELEGATED_PLAN_HALTED',
|
|
116
192
|
/** A CDP or chain read/write failed outright; no upstream body is relayed. */
|
|
117
193
|
'DELEGATION_UPSTREAM_FAILED',
|
|
194
|
+
/** `otto_prepare_yield_deposit`: the executableMarketId names no executable Morpho market in this server's registry. */
|
|
195
|
+
'YIELD_MARKET_UNKNOWN',
|
|
196
|
+
/** The caller's registryCommitment is not the commitment recomputed over the CURRENT registry record (stale or foreign row). */
|
|
197
|
+
'YIELD_REGISTRY_MISMATCH',
|
|
198
|
+
/** The vault's own previewDeposit could not be read, so deposit parity cannot be checked; nothing is prepared. */
|
|
199
|
+
'YIELD_PREVIEW_UNAVAILABLE',
|
|
200
|
+
/** The route quotes fewer shares than the vault's own previewDeposit by more than the pinned tolerance. */
|
|
201
|
+
'YIELD_DEPOSIT_PARITY',
|
|
202
|
+
/** `otto_prepare_yield_withdraw`: the route quotes less USDC than the vault's own previewRedeem by more than the pinned tolerance. */
|
|
203
|
+
'YIELD_WITHDRAW_PARITY',
|
|
204
|
+
/** A delegated vault withdraw under a permission minted before cap ruleset v2 (no consent to withdraws); re-mint. */
|
|
205
|
+
'DELEGATION_WITHDRAW_UNCONSENTED',
|
|
206
|
+
/** `otto_prepare_stock_buy`: the executableEquityId names no executable equity in this server's registry (or the row fails its own pins). */
|
|
207
|
+
'EQUITY_UNKNOWN',
|
|
208
|
+
/** The caller's registryCommitment is not the commitment recomputed over the CURRENT equity record (stale or foreign row). */
|
|
209
|
+
'EQUITY_REGISTRY_MISMATCH',
|
|
210
|
+
/** The equity's Chainlink feed could not be read (or reads non-positive), so price parity cannot be checked; nothing is prepared. */
|
|
211
|
+
'EQUITY_ORACLE_UNAVAILABLE',
|
|
212
|
+
/** The equity's Chainlink mark is older than the pinned frozen threshold (feeds hold their last value off-hours); a delegated buy waits for a live reference. */
|
|
213
|
+
'EQUITY_ORACLE_FROZEN',
|
|
214
|
+
/** The route's gross implied price exceeds the equity's own Chainlink mark by more than the pinned tolerance. */
|
|
215
|
+
'STOCK_BUY_PARITY',
|
|
118
216
|
];
|
|
119
217
|
/** A fail-closed, machine-readable refusal. No upstream response bodies or secrets are retained. */
|
|
120
218
|
export class ExecutionRefusal extends Error {
|
|
@@ -11,7 +11,8 @@ export { swapIntentDigestInput } from './execution-intent.js';
|
|
|
11
11
|
export { artifactCommitmentDigest, assertArtifactAccountBinding, assertArtifactCommitment, assertArtifactEnvelope, assertArtifactFresh, assertArtifactPayloadDigest, executionDigest, MAX_ARTIFACT_TTL_MS, } from './execution-types.js';
|
|
12
12
|
export type { AccountProfile, ExecutionArtifact, ExecutionEnvelope, ExecutionPayload, } from './execution-types.js';
|
|
13
13
|
export { ExecutionRefusal, EXECUTION_REFUSAL_CODES } from './execution-errors.js';
|
|
14
|
-
export { MODEL_B_DELEGATION_EXPIRY_DEFAULT_DAYS, MODEL_B_DELEGATION_EXPIRY_MAX_DAYS, MODEL_B_INPUT_TOKENS_V1, MODEL_B_PER_SWAP_CAP_USD, boundedDelegationExpiry, MODEL_B_POLICY_DIGEST_V1, MODEL_B_POLICY_NAME_V1, modelBProjectPolicyBody, modelBProjectPolicyRules, policyRulesDigest, readBackModelBFence, type DelegationPolicyRecord, type ModelBFenceReadBack, } from './execution-delegation-policy.js';
|
|
14
|
+
export { MODEL_B_DELEGATION_EXPIRY_DEFAULT_DAYS, MODEL_B_DELEGATION_EXPIRY_MAX_DAYS, MODEL_B_DELEGATION_EXPIRY_MIN_DAYS, MODEL_B_INPUT_TOKENS_V1, MODEL_B_MINT_EXPIRY_FILLER_MS, mintExpiryFor, MODEL_B_PER_SWAP_CAP_MIN_USD, MODEL_B_PER_SWAP_CAP_USD, boundedDelegationExpiry, boundedPerSwapCap, assertWithinUserPerSwapCap, MODEL_B_POLICY_DIGEST_V1, MODEL_B_POLICY_NAME_V1, MODEL_B_POLICY_NAME_V2, MODEL_B_POLICY_NAME, MODEL_B_POLICY_DIGEST_V2, MODEL_B_POLICY_DIGEST, MODEL_B_POLICY_DIGEST_BY_NAME, MODEL_B_POLICY_ACCEPTED_VERSIONS, MODEL_B_WITHDRAW_TOKENS_V2, MODEL_B_WITHDRAW_SHARE_CAP_ATOMIC, assertWithdrawWithinModelBFence, fenceIsCurrent, modelBProjectPolicyBody, modelBProjectPolicyRules, policyRulesDigest, readBackModelBFence, type DelegationPolicyRecord, type ModelBFenceReadBack, } from './execution-delegation-policy.js';
|
|
15
15
|
export { DELEGATED_EXECUTION_TOOL_NAME, DELEGATION_AUTH_HEADER, SUBMIT_UNDER_DELEGATION_INPUT_SCHEMA, DELEGATED_SEND_RESULT_SCHEMA, type DelegatedSendResult, type SubmitUnderDelegationInput, } from './execution-delegated-definition.js';
|
|
16
16
|
export { DEFAULT_LIFI_AGGREGATE_FEE_CAP_RATE, DEFAULT_LIFI_FEE_BPS, DEFAULT_LIFI_FEE_CONFIG_VERSION, DEFAULT_LIFI_PLATFORM_FEE_RECIPIENT, DEFAULT_OTTO_LIFI_FEE_RECIPIENT, LIFI_DIAMOND_ADDRESS, LIFI_FEE_FORWARDERS_BY_CHAIN, LIFI_FEE_FORWARDER_V1, LIFI_FEE_FORWARDER_V1_MAINNET, LIFI_FEE_FORWARDER_V2, LIFI_INTEGRATOR, isReviewedLiFiFeeForwarder, reviewedLiFiFeeForwarders, unreviewedFeeForwarderMessage, } from './execution-config.js';
|
|
17
|
-
export { DELEGATION_ADMIN_TOOL_NAMES, DELEGATION_FENCE_INPUT_SCHEMA, DELEGATION_FENCE_RESULT_SCHEMA, DELEGATION_FENCE_STATUS_INPUT_SCHEMA, DELEGATION_FENCE_STATUS_RESULT_SCHEMA, DELEGATION_FENCE_STATUS_TOOL_NAME, DELEGATION_FENCE_TOOL_NAME, DELEGATION_REVOKE_INPUT_SCHEMA, DELEGATION_REVOKE_RESULT_SCHEMA, DELEGATION_REVOKE_TOOL_NAME, DELEGATION_STATUS_INPUT_SCHEMA, DELEGATION_STATUS_RESULT_SCHEMA, DELEGATION_STATUS_TOOL_NAME, type DelegationFenceResult, type DelegationFenceStatusResult, type DelegationRevokeResult, type DelegationStatusResult, } from './execution-delegation-admin-definition.js';
|
|
17
|
+
export { DELEGATION_ADMIN_TOOL_NAMES, DELEGATION_CAP_INPUT_SCHEMA, DELEGATION_CAP_RESULT_SCHEMA, DELEGATION_CAP_TOOL_NAME, DELEGATION_FENCE_INPUT_SCHEMA, DELEGATION_FENCE_RESULT_SCHEMA, DELEGATION_FENCE_STATUS_INPUT_SCHEMA, DELEGATION_FENCE_STATUS_RESULT_SCHEMA, DELEGATION_FENCE_STATUS_TOOL_NAME, DELEGATION_FENCE_TOOL_NAME, DELEGATION_REVOKE_INPUT_SCHEMA, DELEGATION_REVOKE_RESULT_SCHEMA, DELEGATION_REVOKE_TOOL_NAME, DELEGATION_STATUS_INPUT_SCHEMA, DELEGATION_STATUS_RESULT_SCHEMA, DELEGATION_STATUS_TOOL_NAME, type DelegationCapResult, type DelegationFenceResult, type DelegationFenceStatusResult, type DelegationRevokeResult, type DelegationStatusResult, } from './execution-delegation-admin-definition.js';
|
|
18
|
+
export { CAP_EXPIRY_MAX_OFFSET_STEPS, DELEGATION_CAP_STORE_PATH_ENV, DelegationCapExpiryUnavailableError, MINT_KEY_PATTERN, createDelegationCapStore, firstFreeExpiryMs, inMemoryDelegationCapStore, resolveDelegationCapStorePath, unavailableDelegationCapStore, wholeSecondMs, type DelegationCapRecord, type DelegationCapReservation, type DelegationCapStore, } from './execution-delegation-cap-store.js';
|
package/dist/execution-index.js
CHANGED
|
@@ -9,8 +9,9 @@ export { EXECUTION_ARTIFACT_SCHEMA, EXECUTION_ENVELOPE_SCHEMA, PREPARE_SWAP_INPU
|
|
|
9
9
|
export { swapIntentDigestInput } from './execution-intent.js';
|
|
10
10
|
export { artifactCommitmentDigest, assertArtifactAccountBinding, assertArtifactCommitment, assertArtifactEnvelope, assertArtifactFresh, assertArtifactPayloadDigest, executionDigest, MAX_ARTIFACT_TTL_MS, } from './execution-types.js';
|
|
11
11
|
export { ExecutionRefusal, EXECUTION_REFUSAL_CODES } from './execution-errors.js';
|
|
12
|
-
export { MODEL_B_DELEGATION_EXPIRY_DEFAULT_DAYS, MODEL_B_DELEGATION_EXPIRY_MAX_DAYS, MODEL_B_INPUT_TOKENS_V1, MODEL_B_PER_SWAP_CAP_USD, boundedDelegationExpiry, MODEL_B_POLICY_DIGEST_V1, MODEL_B_POLICY_NAME_V1, modelBProjectPolicyBody, modelBProjectPolicyRules, policyRulesDigest, readBackModelBFence, } from './execution-delegation-policy.js';
|
|
12
|
+
export { MODEL_B_DELEGATION_EXPIRY_DEFAULT_DAYS, MODEL_B_DELEGATION_EXPIRY_MAX_DAYS, MODEL_B_DELEGATION_EXPIRY_MIN_DAYS, MODEL_B_INPUT_TOKENS_V1, MODEL_B_MINT_EXPIRY_FILLER_MS, mintExpiryFor, MODEL_B_PER_SWAP_CAP_MIN_USD, MODEL_B_PER_SWAP_CAP_USD, boundedDelegationExpiry, boundedPerSwapCap, assertWithinUserPerSwapCap, MODEL_B_POLICY_DIGEST_V1, MODEL_B_POLICY_NAME_V1, MODEL_B_POLICY_NAME_V2, MODEL_B_POLICY_NAME, MODEL_B_POLICY_DIGEST_V2, MODEL_B_POLICY_DIGEST, MODEL_B_POLICY_DIGEST_BY_NAME, MODEL_B_POLICY_ACCEPTED_VERSIONS, MODEL_B_WITHDRAW_TOKENS_V2, MODEL_B_WITHDRAW_SHARE_CAP_ATOMIC, assertWithdrawWithinModelBFence, fenceIsCurrent, modelBProjectPolicyBody, modelBProjectPolicyRules, policyRulesDigest, readBackModelBFence, } from './execution-delegation-policy.js';
|
|
13
13
|
export { DELEGATED_EXECUTION_TOOL_NAME, DELEGATION_AUTH_HEADER, SUBMIT_UNDER_DELEGATION_INPUT_SCHEMA, DELEGATED_SEND_RESULT_SCHEMA, } from './execution-delegated-definition.js';
|
|
14
14
|
export { DEFAULT_LIFI_AGGREGATE_FEE_CAP_RATE, DEFAULT_LIFI_FEE_BPS, DEFAULT_LIFI_FEE_CONFIG_VERSION, DEFAULT_LIFI_PLATFORM_FEE_RECIPIENT, DEFAULT_OTTO_LIFI_FEE_RECIPIENT, LIFI_DIAMOND_ADDRESS, LIFI_FEE_FORWARDERS_BY_CHAIN, LIFI_FEE_FORWARDER_V1, LIFI_FEE_FORWARDER_V1_MAINNET, LIFI_FEE_FORWARDER_V2, LIFI_INTEGRATOR, isReviewedLiFiFeeForwarder, reviewedLiFiFeeForwarders, unreviewedFeeForwarderMessage, } from './execution-config.js';
|
|
15
|
-
export { DELEGATION_ADMIN_TOOL_NAMES, DELEGATION_FENCE_INPUT_SCHEMA, DELEGATION_FENCE_RESULT_SCHEMA, DELEGATION_FENCE_STATUS_INPUT_SCHEMA, DELEGATION_FENCE_STATUS_RESULT_SCHEMA, DELEGATION_FENCE_STATUS_TOOL_NAME, DELEGATION_FENCE_TOOL_NAME, DELEGATION_REVOKE_INPUT_SCHEMA, DELEGATION_REVOKE_RESULT_SCHEMA, DELEGATION_REVOKE_TOOL_NAME, DELEGATION_STATUS_INPUT_SCHEMA, DELEGATION_STATUS_RESULT_SCHEMA, DELEGATION_STATUS_TOOL_NAME, } from './execution-delegation-admin-definition.js';
|
|
15
|
+
export { DELEGATION_ADMIN_TOOL_NAMES, DELEGATION_CAP_INPUT_SCHEMA, DELEGATION_CAP_RESULT_SCHEMA, DELEGATION_CAP_TOOL_NAME, DELEGATION_FENCE_INPUT_SCHEMA, DELEGATION_FENCE_RESULT_SCHEMA, DELEGATION_FENCE_STATUS_INPUT_SCHEMA, DELEGATION_FENCE_STATUS_RESULT_SCHEMA, DELEGATION_FENCE_STATUS_TOOL_NAME, DELEGATION_FENCE_TOOL_NAME, DELEGATION_REVOKE_INPUT_SCHEMA, DELEGATION_REVOKE_RESULT_SCHEMA, DELEGATION_REVOKE_TOOL_NAME, DELEGATION_STATUS_INPUT_SCHEMA, DELEGATION_STATUS_RESULT_SCHEMA, DELEGATION_STATUS_TOOL_NAME, } from './execution-delegation-admin-definition.js';
|
|
16
|
+
export { CAP_EXPIRY_MAX_OFFSET_STEPS, DELEGATION_CAP_STORE_PATH_ENV, DelegationCapExpiryUnavailableError, MINT_KEY_PATTERN, createDelegationCapStore, firstFreeExpiryMs, inMemoryDelegationCapStore, resolveDelegationCapStorePath, unavailableDelegationCapStore, wholeSecondMs, } from './execution-delegation-cap-store.js';
|
|
16
17
|
//# sourceMappingURL=execution-index.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
-
import { ensureDelegationFence, readDelegationStatus, revokeDelegation } from './execution-delegation-admin.js';
|
|
3
|
+
import { ensureDelegationFence, readDelegationStatus, reserveDelegationCap, revokeDelegation } from './execution-delegation-admin.js';
|
|
4
4
|
import { type ExecutionToolDefinition } from './execution-tool-definitions.js';
|
|
5
5
|
import { type ExecutionToolRuntime } from './execution-tools.js';
|
|
6
6
|
export declare const EXECUTION_TOOL_ANNOTATIONS: Readonly<{
|
|
@@ -11,6 +11,7 @@ export declare const EXECUTION_TOOL_ANNOTATIONS: Readonly<{
|
|
|
11
11
|
}>;
|
|
12
12
|
export declare function executionErrorResult(error: unknown): CallToolResult;
|
|
13
13
|
export declare function callExecutionDefinition(runtime: ExecutionToolRuntime, definition: ExecutionToolDefinition, rawInput: unknown, signal?: AbortSignal): Promise<CallToolResult>;
|
|
14
|
+
export declare function callYieldDefinition(prepare: (runtime: ExecutionToolRuntime, rawInput: unknown, signal?: AbortSignal) => Promise<unknown>, runtime: ExecutionToolRuntime, rawInput: unknown, signal?: AbortSignal): Promise<CallToolResult>;
|
|
14
15
|
/** A failure that is not a refusal is an upstream fault; nothing of it is relayed. */
|
|
15
16
|
export declare function delegatedErrorResult(error: unknown): CallToolResult;
|
|
16
17
|
/** The caller comes ONLY from the request headers; a value placed in the arguments is never consulted. */
|
|
@@ -47,7 +48,7 @@ declare const DELEGATION_ADMIN_TOOLS: readonly [{
|
|
|
47
48
|
readonly definition: Readonly<{
|
|
48
49
|
name: "otto_delegation_status";
|
|
49
50
|
title: "Read one end user's delegation status (token-bound)";
|
|
50
|
-
description: `Read back, for the end user identified by a CDP access token, their CDP accounts, whether the Model-B fence is present, and whether an active delegation reads back (with its expiry). Arguments: { accessToken } \u2014 the server resolves the user from the token; a client-sent user id is refused. Output is bound to that user; nothing about any other user is returned. ${string}`;
|
|
51
|
+
description: `Read back, for the end user identified by a CDP access token, their CDP accounts, whether the Model-B fence is present, and whether an active delegation reads back (with its expiry and the per-swap cap recorded for it). Arguments: { accessToken } \u2014 the server resolves the user from the token; a client-sent user id is refused. delegation.per_swap_cap_usd is the number THIS user chose at mint time; it is ABSENT when no cap is recorded for the grant, which means the delegated send will refuse it \u2014 say so rather than showing the shared ceiling. fence.per_swap_cap_usd is that shared ceiling and is never a per-user number. Output is bound to that user; nothing about any other user is returned. ${string}`;
|
|
51
52
|
inputSchema: import("zod").ZodObject<{}, import("zod/v4/core").$loose>;
|
|
52
53
|
strictInputSchema: import("zod").ZodObject<{
|
|
53
54
|
accessToken: import("zod").ZodString;
|
|
@@ -64,8 +65,11 @@ declare const DELEGATION_ADMIN_TOOLS: readonly [{
|
|
|
64
65
|
delegation: import("zod").ZodObject<{
|
|
65
66
|
active: import("zod").ZodBoolean;
|
|
66
67
|
expires_at: import("zod").ZodOptional<import("zod").ZodString>;
|
|
68
|
+
per_swap_cap_usd: import("zod").ZodOptional<import("zod").ZodNumber>;
|
|
67
69
|
}, import("zod/v4/core").$strict>;
|
|
68
70
|
revocation_requested_here: import("zod").ZodBoolean;
|
|
71
|
+
ruleset_version: import("zod").ZodNumber;
|
|
72
|
+
mint_enabled: import("zod").ZodBoolean;
|
|
69
73
|
}, import("zod/v4/core").$strict>;
|
|
70
74
|
annotations: Readonly<{
|
|
71
75
|
readOnlyHint: true;
|
|
@@ -104,6 +108,51 @@ declare const DELEGATION_ADMIN_TOOLS: readonly [{
|
|
|
104
108
|
}>;
|
|
105
109
|
}>;
|
|
106
110
|
readonly run: typeof revokeDelegation;
|
|
111
|
+
}, {
|
|
112
|
+
readonly definition: Readonly<{
|
|
113
|
+
name: "otto_delegation_cap";
|
|
114
|
+
title: "Reserve the expiry a delegation is minted with, record the chosen per-swap cap against it, then confirm the mint";
|
|
115
|
+
description: `TWO PHASES, both header authed, both resolving the end user from the access token. { phase: "reserve", accessToken, perSwapCapUsd, requestedDays, mintKey } picks a whole-second expiry instant at or BEFORE the requested duration that no durable row for this user already occupies, writes the chosen cap (whole USD, 1\u20265000) against it, and answers { expires_at, mint_expires_at, per_swap_cap_usd, recorded }. Call createDelegation with mint_expires_at EXACTLY as given \u2014 it is the reserved instant plus a one-millisecond filler, because CDP refuses an expiry ending .000 and truncates the filler away \u2014 and with mintKey as the idempotencyKey. Then { phase: "confirm", accessToken, mintKey } re-reads the grant from CDP and refuses (DELEGATION_CAP_GRANT_CHANGED) unless the instant CDP reports IS the reserved one, revoking the fresh grant when it is not. Why this shape: Coinbase issues no grant id and a send can learn nothing about a live grant except its expiry, so the expiry is made an identity BY CONSTRUCTION \u2014 reserved server-side, recorded before the mint, asserted after it. Until a reservation exists, otto_submit_under_delegation refuses that delegation (DELEGATION_CAP_UNRECORDED) rather than falling back to the 5000 USD ceiling. A reservation is IMMUTABLE per mint key \u2014 pressing again returns the same instant and the cap already in force, so raising a cap means minting a new permission (which the user is told replaces the old one). Coinbase's engine cannot hold this number: end-user accounts are fenced by ONE project-scope policy shared by every delegated user, so a lower per-user cap is enforced by Otto's server alone. ${string}`;
|
|
116
|
+
inputSchema: import("zod").ZodObject<{}, import("zod/v4/core").$loose>;
|
|
117
|
+
strictInputSchema: import("zod").ZodDiscriminatedUnion<[import("zod").ZodObject<{
|
|
118
|
+
phase: import("zod").ZodLiteral<"reserve">;
|
|
119
|
+
accessToken: import("zod").ZodString;
|
|
120
|
+
perSwapCapUsd: import("zod").ZodNumber;
|
|
121
|
+
requestedDays: import("zod").ZodNumber;
|
|
122
|
+
mintKey: import("zod").ZodString;
|
|
123
|
+
}, import("zod/v4/core").$strict>, import("zod").ZodObject<{
|
|
124
|
+
phase: import("zod").ZodLiteral<"confirm">;
|
|
125
|
+
accessToken: import("zod").ZodString;
|
|
126
|
+
mintKey: import("zod").ZodString;
|
|
127
|
+
}, import("zod/v4/core").$strict>], "phase">;
|
|
128
|
+
outputSchema: import("zod").ZodObject<{
|
|
129
|
+
phase: import("zod").ZodEnum<{
|
|
130
|
+
reserve: "reserve";
|
|
131
|
+
confirm: "confirm";
|
|
132
|
+
}>;
|
|
133
|
+
user_id: import("zod").ZodString;
|
|
134
|
+
expires_at: import("zod").ZodString;
|
|
135
|
+
mint_expires_at: import("zod").ZodOptional<import("zod").ZodString>;
|
|
136
|
+
per_swap_cap_usd: import("zod").ZodNumber;
|
|
137
|
+
state: import("zod").ZodEnum<{
|
|
138
|
+
legacy: "legacy";
|
|
139
|
+
reserved: "reserved";
|
|
140
|
+
confirmed_active: "confirmed_active";
|
|
141
|
+
superseded: "superseded";
|
|
142
|
+
revoked: "revoked";
|
|
143
|
+
expired: "expired";
|
|
144
|
+
}>;
|
|
145
|
+
ruleset_version: import("zod").ZodNumber;
|
|
146
|
+
recorded: import("zod").ZodBoolean;
|
|
147
|
+
}, import("zod/v4/core").$strict>;
|
|
148
|
+
annotations: Readonly<{
|
|
149
|
+
readOnlyHint: false;
|
|
150
|
+
destructiveHint: false;
|
|
151
|
+
idempotentHint: true;
|
|
152
|
+
openWorldHint: true;
|
|
153
|
+
}>;
|
|
154
|
+
}>;
|
|
155
|
+
readonly run: typeof reserveDelegationCap;
|
|
107
156
|
}];
|
|
108
157
|
export declare function callDelegationAdminDefinition(runtime: ExecutionToolRuntime, run: (typeof DELEGATION_ADMIN_TOOLS)[number]['run'], rawInput: unknown, headers: Readonly<Record<string, string | string[] | undefined>> | undefined): Promise<CallToolResult>;
|
|
109
158
|
export interface ExecutionCallAdmission {
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { ExecutionRefusal, isExecutionRefusal } from './execution-errors.js';
|
|
2
2
|
import { DELEGATED_EXECUTION_TOOL_DEFINITION, DELEGATED_TOOL_ANNOTATIONS, } from './execution-delegated-definition.js';
|
|
3
3
|
import { callerFromHeaders, submitUnderDelegation } from './execution-delegation.js';
|
|
4
|
-
import { DELEGATION_FENCE_STATUS_TOOL_DEFINITION, DELEGATION_FENCE_TOOL_DEFINITION, DELEGATION_REVOKE_TOOL_DEFINITION, DELEGATION_STATUS_TOOL_DEFINITION, } from './execution-delegation-admin-definition.js';
|
|
5
|
-
import { ensureDelegationFence, readDelegationFenceStatus, readDelegationStatus, revokeDelegation, } from './execution-delegation-admin.js';
|
|
4
|
+
import { DELEGATION_CAP_TOOL_DEFINITION, DELEGATION_FENCE_STATUS_TOOL_DEFINITION, DELEGATION_FENCE_TOOL_DEFINITION, DELEGATION_REVOKE_TOOL_DEFINITION, DELEGATION_STATUS_TOOL_DEFINITION, } from './execution-delegation-admin-definition.js';
|
|
5
|
+
import { ensureDelegationFence, readDelegationFenceStatus, readDelegationStatus, reserveDelegationCap, revokeDelegation, } from './execution-delegation-admin.js';
|
|
6
6
|
import { EXECUTION_TOOL_DEFINITIONS, } from './execution-tool-definitions.js';
|
|
7
|
+
import { YIELD_DEPOSIT_TOOL_DEFINITION } from './yield-deposit-definition.js';
|
|
8
|
+
import { prepareYieldDeposit } from './yield-deposit.js';
|
|
9
|
+
import { STOCK_BUY_TOOL_DEFINITION } from './stock-buy-definition.js';
|
|
10
|
+
import { prepareStockBuy } from './stock-buy.js';
|
|
11
|
+
import { YIELD_WITHDRAW_TOOL_DEFINITION } from './yield-withdraw-definition.js';
|
|
12
|
+
import { prepareYieldWithdraw } from './yield-withdraw.js';
|
|
7
13
|
import { prepareBridge, preparePerpOrder, preparePolymarketOrder, prepareSwap, } from './execution-tools.js';
|
|
8
14
|
export const EXECUTION_TOOL_ANNOTATIONS = Object.freeze({
|
|
9
15
|
readOnlyHint: true,
|
|
@@ -46,6 +52,18 @@ export async function callExecutionDefinition(runtime, definition, rawInput, sig
|
|
|
46
52
|
return executionErrorResult(error);
|
|
47
53
|
}
|
|
48
54
|
}
|
|
55
|
+
export async function callYieldDefinition(prepare, runtime, rawInput, signal) {
|
|
56
|
+
try {
|
|
57
|
+
const output = await prepare(runtime, rawInput, signal);
|
|
58
|
+
return {
|
|
59
|
+
content: [{ type: 'text', text: stringify(output) }],
|
|
60
|
+
structuredContent: output,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
return executionErrorResult(error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
49
67
|
function delegatedSuccessResult(result) {
|
|
50
68
|
return {
|
|
51
69
|
content: [{ type: 'text', text: stringify(result) }],
|
|
@@ -72,6 +90,7 @@ const DELEGATION_ADMIN_TOOLS = [
|
|
|
72
90
|
{ definition: DELEGATION_FENCE_TOOL_DEFINITION, run: ensureDelegationFence },
|
|
73
91
|
{ definition: DELEGATION_STATUS_TOOL_DEFINITION, run: readDelegationStatus },
|
|
74
92
|
{ definition: DELEGATION_REVOKE_TOOL_DEFINITION, run: revokeDelegation },
|
|
93
|
+
{ definition: DELEGATION_CAP_TOOL_DEFINITION, run: reserveDelegationCap },
|
|
75
94
|
];
|
|
76
95
|
export async function callDelegationAdminDefinition(runtime, run, rawInput, headers) {
|
|
77
96
|
try {
|
|
@@ -107,6 +126,30 @@ export function registerExecutionTools(server, runtime, admission) {
|
|
|
107
126
|
return admission ? admission.run(call) : call();
|
|
108
127
|
});
|
|
109
128
|
}
|
|
129
|
+
// The Yield Copilot deposit constructor: registered where the delegated services are (it needs the
|
|
130
|
+
// rig's Base client for the vault's previewDeposit), read-only like every other prepare tool.
|
|
131
|
+
// Registered wherever the delegated set is; without a vault reader the tool refuses by name.
|
|
132
|
+
if (runtime.delegation) {
|
|
133
|
+
for (const yieldTool of [
|
|
134
|
+
{ definition: YIELD_DEPOSIT_TOOL_DEFINITION, prepare: prepareYieldDeposit },
|
|
135
|
+
{ definition: YIELD_WITHDRAW_TOOL_DEFINITION, prepare: prepareYieldWithdraw },
|
|
136
|
+
// The tokenized-stock buy constructor rides the same registration block: it needs the rig's
|
|
137
|
+
// Base client for the equity's Chainlink mark; without a reader it refuses by name.
|
|
138
|
+
{ definition: STOCK_BUY_TOOL_DEFINITION, prepare: prepareStockBuy },
|
|
139
|
+
]) {
|
|
140
|
+
const { definition, prepare } = yieldTool;
|
|
141
|
+
server.registerTool(definition.name, {
|
|
142
|
+
title: definition.title,
|
|
143
|
+
description: definition.description + freshness,
|
|
144
|
+
inputSchema: definition.inputSchema,
|
|
145
|
+
outputSchema: definition.outputSchema,
|
|
146
|
+
annotations: EXECUTION_TOOL_ANNOTATIONS,
|
|
147
|
+
}, (input, extra) => {
|
|
148
|
+
const call = () => callYieldDefinition(prepare, runtime, input, extra.signal);
|
|
149
|
+
return admission ? admission.run(call) : call();
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
110
153
|
// The one tool that submits: registered only when the runtime carries the delegated services, with
|
|
111
154
|
// its own (non-read-only) annotations, through the same per-identity admission as the prepare tools.
|
|
112
155
|
if (runtime.delegation) {
|