otto-intel-mcp 0.1.2 โ 0.1.4
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 +76 -18
- package/dist/account-userop-policy-definition.d.ts +14 -0
- package/dist/account-userop-policy-definition.js +12 -0
- package/dist/account-userop-policy-relation.d.ts +371 -0
- package/dist/account-userop-policy-relation.js +87 -0
- package/dist/adapter/cdp-signer.d.ts +76 -9
- package/dist/adapter/cdp-signer.js +274 -33
- 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/adapter-cdp-index.d.ts +1 -1
- 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-account-gas-quote.d.ts +141 -0
- package/dist/execution-account-gas-quote.js +443 -0
- package/dist/execution-account-gas-review.d.ts +123 -0
- package/dist/execution-account-gas-review.js +104 -0
- package/dist/execution-account-gas-runtime.d.ts +33 -0
- package/dist/execution-account-gas-runtime.js +131 -0
- package/dist/execution-account-gas-view.d.ts +936 -0
- package/dist/execution-account-gas-view.js +56 -0
- package/dist/execution-config.d.ts +3 -0
- package/dist/execution-config.js +4 -0
- package/dist/execution-delegated-definition.d.ts +7 -6
- package/dist/execution-delegated-definition.js +13 -2
- package/dist/execution-delegation-admin-definition.d.ts +127 -5
- package/dist/execution-delegation-admin-definition.js +155 -5
- package/dist/execution-delegation-admin.d.ts +48 -2
- package/dist/execution-delegation-admin.js +422 -12
- package/dist/execution-delegation-cap-store.d.ts +386 -0
- package/dist/execution-delegation-cap-store.js +1150 -0
- package/dist/execution-delegation-exit-definition.d.ts +309 -0
- package/dist/execution-delegation-exit-definition.js +204 -0
- package/dist/execution-delegation-exit-store.d.ts +114 -0
- package/dist/execution-delegation-exit-store.js +671 -0
- package/dist/execution-delegation-exit.d.ts +4 -0
- package/dist/execution-delegation-exit.js +614 -0
- package/dist/execution-delegation-finality.d.ts +22 -0
- package/dist/execution-delegation-finality.js +90 -0
- package/dist/execution-delegation-operation-store.d.ts +129 -0
- package/dist/execution-delegation-operation-store.js +1107 -0
- package/dist/execution-delegation-operations-definition.d.ts +282 -0
- package/dist/execution-delegation-operations-definition.js +117 -0
- package/dist/execution-delegation-operations.d.ts +6 -0
- package/dist/execution-delegation-operations.js +282 -0
- package/dist/execution-delegation-policy.d.ts +154 -8
- package/dist/execution-delegation-policy.js +273 -40
- package/dist/execution-delegation.d.ts +51 -0
- package/dist/execution-delegation.js +441 -31
- package/dist/execution-errors.d.ts +1 -1
- package/dist/execution-errors.js +122 -0
- package/dist/execution-index.d.ts +7 -2
- package/dist/execution-index.js +7 -2
- package/dist/execution-journal-schema.d.ts +96 -0
- package/dist/execution-journal-schema.js +368 -0
- package/dist/execution-journal-tables.d.ts +14 -0
- package/dist/execution-journal-tables.js +227 -0
- package/dist/execution-policy-compatibility.d.ts +34 -0
- package/dist/execution-policy-compatibility.js +76 -0
- package/dist/execution-registration.d.ts +54 -2
- package/dist/execution-registration.js +151 -5
- package/dist/execution-sponsored-account-profile.d.ts +32 -0
- package/dist/execution-sponsored-account-profile.js +59 -0
- package/dist/execution-sponsored-account.d.ts +1519 -0
- package/dist/execution-sponsored-account.js +222 -0
- package/dist/execution-sponsored-definition.d.ts +1713 -0
- package/dist/execution-sponsored-definition.js +185 -0
- package/dist/execution-sponsored-gas-policy.d.ts +27 -0
- package/dist/execution-sponsored-gas-policy.js +110 -0
- package/dist/execution-sponsored-hosted.d.ts +3 -0
- package/dist/execution-sponsored-hosted.js +71 -0
- package/dist/execution-sponsored-index.d.ts +1322 -0
- package/dist/execution-sponsored-index.js +102 -0
- package/dist/execution-sponsored-mcp-definition.d.ts +1662 -0
- package/dist/execution-sponsored-mcp-definition.js +94 -0
- package/dist/execution-sponsored-receipt.d.ts +47 -0
- package/dist/execution-sponsored-receipt.js +74 -0
- package/dist/execution-sponsored-registration.d.ts +14 -0
- package/dist/execution-sponsored-registration.js +77 -0
- package/dist/execution-sponsored-runtime.d.ts +84 -0
- package/dist/execution-sponsored-runtime.js +149 -0
- package/dist/execution-sponsored-settlement.d.ts +15 -0
- package/dist/execution-sponsored-settlement.js +88 -0
- package/dist/execution-sponsored-store.d.ts +67 -0
- package/dist/execution-sponsored-store.js +517 -0
- package/dist/execution-sponsored-transport.d.ts +90 -0
- package/dist/execution-sponsored-transport.js +180 -0
- package/dist/execution-tool-definitions.d.ts +21 -21
- package/dist/execution-tool-definitions.js +0 -7
- package/dist/execution-tools.d.ts +53 -1
- package/dist/execution-tools.js +171 -13
- package/dist/execution-worker-guard.d.ts +28 -0
- package/dist/execution-worker-guard.js +124 -0
- package/dist/hybrid-yield-index.d.ts +10 -0
- package/dist/hybrid-yield-index.js +11 -0
- package/dist/hybrid-yield-markets.d.ts +96 -0
- package/dist/hybrid-yield-markets.js +124 -0
- package/dist/hybrid-yield.d.ts +411 -0
- package/dist/hybrid-yield.js +155 -0
- package/dist/hyperliquid-order-builder.js +4 -0
- package/dist/lifi-execution-client.d.ts +6 -0
- package/dist/lifi-execution-client.js +3 -0
- package/dist/lp-userop-account.d.ts +461 -0
- package/dist/lp-userop-account.js +417 -0
- package/dist/lp-userop-authority.d.ts +49 -0
- package/dist/lp-userop-authority.js +211 -0
- package/dist/lp-userop-definition.d.ts +233 -0
- package/dist/lp-userop-definition.js +85 -0
- package/dist/lp-userop-hosted.d.ts +3 -0
- package/dist/lp-userop-hosted.js +73 -0
- package/dist/lp-userop-index.d.ts +1569 -0
- package/dist/lp-userop-index.js +112 -0
- package/dist/lp-userop-mcp-definition.d.ts +1220 -0
- package/dist/lp-userop-mcp-definition.js +46 -0
- package/dist/lp-userop-operation.d.ts +1230 -0
- package/dist/lp-userop-operation.js +140 -0
- package/dist/lp-userop-registration.d.ts +14 -0
- package/dist/lp-userop-registration.js +70 -0
- package/dist/lp-userop-settlement.d.ts +89 -0
- package/dist/lp-userop-settlement.js +184 -0
- package/dist/lp-userop.d.ts +643 -0
- package/dist/lp-userop.js +343 -0
- package/dist/native-yield-account-definition.d.ts +1192 -0
- package/dist/native-yield-account-definition.js +232 -0
- package/dist/native-yield-account-gas.d.ts +58 -0
- package/dist/native-yield-account-gas.js +228 -0
- package/dist/native-yield-account-index.d.ts +23 -0
- package/dist/native-yield-account-index.js +28 -0
- package/dist/native-yield-account-policy.d.ts +168 -0
- package/dist/native-yield-account-policy.js +525 -0
- package/dist/native-yield-account-rpc.d.ts +8 -0
- package/dist/native-yield-account-rpc.js +128 -0
- package/dist/native-yield-account-runtime.d.ts +71 -0
- package/dist/native-yield-account-runtime.js +125 -0
- package/dist/native-yield-account-store.d.ts +40 -0
- package/dist/native-yield-account-store.js +307 -0
- package/dist/native-yield-account.d.ts +662 -0
- package/dist/native-yield-account.js +371 -0
- package/dist/native-yield-consent-store.d.ts +20 -0
- package/dist/native-yield-consent-store.js +317 -0
- package/dist/native-yield-definition.d.ts +288 -0
- package/dist/native-yield-definition.js +105 -0
- package/dist/native-yield-reader.d.ts +4 -0
- package/dist/native-yield-reader.js +137 -0
- package/dist/native-yield-settlement.d.ts +48 -0
- package/dist/native-yield-settlement.js +167 -0
- package/dist/native-yield-simulation.d.ts +18 -0
- package/dist/native-yield-simulation.js +45 -0
- package/dist/native-yield-userop-account.d.ts +675 -0
- package/dist/native-yield-userop-account.js +357 -0
- package/dist/native-yield-userop-authority.d.ts +214 -0
- package/dist/native-yield-userop-authority.js +296 -0
- package/dist/native-yield-userop-definition.d.ts +331 -0
- package/dist/native-yield-userop-definition.js +105 -0
- package/dist/native-yield-userop-hosted.d.ts +3 -0
- package/dist/native-yield-userop-hosted.js +71 -0
- package/dist/native-yield-userop-index.d.ts +1522 -0
- package/dist/native-yield-userop-index.js +142 -0
- package/dist/native-yield-userop-mcp-definition.d.ts +1356 -0
- package/dist/native-yield-userop-mcp-definition.js +50 -0
- package/dist/native-yield-userop-operation.d.ts +845 -0
- package/dist/native-yield-userop-operation.js +127 -0
- package/dist/native-yield-userop-policy.d.ts +23 -0
- package/dist/native-yield-userop-policy.js +25 -0
- package/dist/native-yield-userop-registration.d.ts +17 -0
- package/dist/native-yield-userop-registration.js +76 -0
- package/dist/native-yield-userop-settlement.d.ts +8 -0
- package/dist/native-yield-userop-settlement.js +79 -0
- package/dist/native-yield-userop-simulation.d.ts +18 -0
- package/dist/native-yield-userop-simulation.js +34 -0
- package/dist/native-yield-userop.d.ts +35 -0
- package/dist/native-yield-userop.js +156 -0
- package/dist/native-yield-verify.d.ts +30 -0
- package/dist/native-yield-verify.js +141 -0
- package/dist/native-yield.d.ts +17 -0
- package/dist/native-yield.js +119 -0
- package/dist/stdio-main.js +0 -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.d.ts +3 -1
- package/dist/tool-definitions.js +55 -2
- package/dist/vendor/lp-v3/lp-v3-atomic-contract.d.ts +445 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-contract.js +90 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-evidence.d.ts +14 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-evidence.js +82 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-index.d.ts +80 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-index.js +19 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-plan.d.ts +43 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-plan.js +112 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-read-contract.d.ts +312 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-read-contract.js +41 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-read-index.d.ts +3 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-read-index.js +3 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-read-rpc.d.ts +49 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-read-rpc.js +88 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-read.d.ts +17 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-read.js +232 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-settlement.d.ts +105 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-settlement.js +162 -0
- package/dist/vendor/lp-v3/lp-v3-collect-protocol-contract.d.ts +606 -0
- package/dist/vendor/lp-v3/lp-v3-collect-protocol-contract.js +59 -0
- package/dist/vendor/lp-v3/lp-v3-collect-protocol-index.d.ts +168 -0
- package/dist/vendor/lp-v3/lp-v3-collect-protocol-index.js +23 -0
- package/dist/vendor/lp-v3/lp-v3-collect-protocol-observation.d.ts +32 -0
- package/dist/vendor/lp-v3/lp-v3-collect-protocol-observation.js +228 -0
- package/dist/vendor/lp-v3/lp-v3-collect-protocol-plan.d.ts +11 -0
- package/dist/vendor/lp-v3/lp-v3-collect-protocol-plan.js +91 -0
- package/dist/vendor/lp-v3/lp-v3-collect-protocol-reconcile.d.ts +9 -0
- package/dist/vendor/lp-v3/lp-v3-collect-protocol-reconcile.js +124 -0
- package/dist/vendor/lp-v3/lp-v3-contract.d.ts +538 -0
- package/dist/vendor/lp-v3/lp-v3-contract.js +137 -0
- package/dist/vendor/lp-v3/lp-v3-evidence.d.ts +25 -0
- package/dist/vendor/lp-v3/lp-v3-evidence.js +168 -0
- package/dist/vendor/lp-v3/lp-v3-plan.d.ts +22 -0
- package/dist/vendor/lp-v3/lp-v3-plan.js +135 -0
- package/dist/vendor/lp-v3/types.d.ts +131 -0
- package/dist/vendor/lp-v3/types.js +6 -0
- package/dist/vendor/lp-v3/v4-math.d.ts +18 -0
- package/dist/vendor/lp-v3/v4-math.js +90 -0
- package/dist/yield-deposit-definition.d.ts +374 -0
- package/dist/yield-deposit-definition.js +123 -0
- package/dist/yield-deposit.d.ts +16 -0
- package/dist/yield-deposit.js +149 -0
- package/dist/yield-route-semantics.d.ts +27 -0
- package/dist/yield-route-semantics.js +71 -0
- package/dist/yield-withdraw-definition.d.ts +357 -0
- package/dist/yield-withdraw-definition.js +90 -0
- package/dist/yield-withdraw.d.ts +11 -0
- package/dist/yield-withdraw.js +122 -0
- package/package.json +239 -7
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* execution-delegation-admin.ts โ the
|
|
2
|
+
* execution-delegation-admin.ts โ the four delegation ADMIN tools on the hosted MCP (PR-1b, seat ruling D10 = (a)).
|
|
3
3
|
*
|
|
4
4
|
* otto_delegation_fence โ the ONLY writer of the Model-B project policy: ensure-by-digest + read-back.
|
|
5
5
|
* otto_delegation_status โ one user's grant/fence read-back, bound to the user a CDP access token resolves to.
|
|
6
6
|
* otto_delegation_revoke โ developer-side revoke with read-back, fail-closed under the rig lock.
|
|
7
|
+
* otto_delegation_cap โ reserves the expiry a delegation is minted with, records the chosen per-swap
|
|
8
|
+
* cap against it, and confirms after the mint that CDP kept that exact instant.
|
|
7
9
|
*
|
|
8
10
|
* Every tool: the request header FIRST (constant-time, before parsing or any CDP read โ the same seam as
|
|
9
11
|
* `otto_submit_under_delegation`), then the strict schema, then the reads. Nothing here logs a token, the
|
|
@@ -44,7 +46,7 @@
|
|
|
44
46
|
* support_reason }` from an operator holding the server secret; the reason is written to the structured log
|
|
45
47
|
* through the boot redactor (URLs and configured secret values never land in the log), the token never is.
|
|
46
48
|
*/
|
|
47
|
-
import { type DelegationFenceResult, type DelegationFenceStatusResult, type DelegationRevokeResult, type DelegationStatusResult } from './execution-delegation-admin-definition.js';
|
|
49
|
+
import { type DelegationCapResult, type DelegationFenceResult, type DelegationFenceStatusResult, type DelegationRevokeResult, type DelegationStatusResult } from './execution-delegation-admin-definition.js';
|
|
48
50
|
import { type DelegationCaller } from './execution-delegation.js';
|
|
49
51
|
import type { ExecutionToolRuntime } from './execution-tools.js';
|
|
50
52
|
/** Read-back after a write: the engine propagates asynchronously (the proof kit waited 3 s flat). */
|
|
@@ -61,6 +63,50 @@ export declare function ensureDelegationFence(runtime: ExecutionToolRuntime, raw
|
|
|
61
63
|
export declare function readDelegationFenceStatus(runtime: ExecutionToolRuntime): Promise<DelegationFenceStatusResult>;
|
|
62
64
|
/** `otto_delegation_status` โ one user's read-back, bound to the token's user. */
|
|
63
65
|
export declare function readDelegationStatus(runtime: ExecutionToolRuntime, rawInput: unknown, caller: DelegationCaller): Promise<DelegationStatusResult>;
|
|
66
|
+
/**
|
|
67
|
+
* `otto_delegation_cap` โ RESERVE the expiry a delegation will be minted with, record the chosen per-swap
|
|
68
|
+
* cap against it, and CONFIRM the mint (Founder word `delegate cap choice`, 2026-09-02; round 3, after the
|
|
69
|
+
* codex gate NO-GO'd rounds 1 and 2 and the seat ruled the collision out rather than documenting it).
|
|
70
|
+
*
|
|
71
|
+
* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
72
|
+
* ๐ด WHY THE ORDER IS RESERVE โ MINT โ CONFIRM, AND NOT MINT โ RECORD
|
|
73
|
+
* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
74
|
+
* The gate's second finding is the whole reason: an expiry is the ONLY thing a send can learn about a live
|
|
75
|
+
* grant, so keying the RECORD by anything else โ r1's expiry-as-key, r2's mint key โ fixes the record and
|
|
76
|
+
* not the send. Its probe minted a $10 replacement carrying the SAME expiry as a standing $5,000 row and
|
|
77
|
+
* watched a plan run at $5,000. Live confirmation of exactly that, on the dev project (2026-09-02T22:17Z):
|
|
78
|
+
* CDP refuses a second mint over a live grant (409 `already_exists`), but after a user-side revoke a
|
|
79
|
+
* re-mint at the SAME instant succeeds and reads back at that identical instant. Two grants for one user
|
|
80
|
+
* CAN share an expiry.
|
|
81
|
+
*
|
|
82
|
+
* So round 3 stops trying to tell them apart and makes them impossible:
|
|
83
|
+
*
|
|
84
|
+
* RESERVE โ the SERVER picks the instant. It walks back from the second the person's chosen duration
|
|
85
|
+
* lands on and takes the first whole second no durable row for this user occupies, then writes the cap
|
|
86
|
+
* against it BEFORE any grant exists. The caller cannot choose the enforcement key, which is the
|
|
87
|
+
* property that makes it unique. The ladder only ever steps EARLIER (seat ruling): a permission must
|
|
88
|
+
* never outlive the duration the person chose, and the page shows the instant it actually got.
|
|
89
|
+
*
|
|
90
|
+
* MINT โ the caller passes `mint_expires_at` to `createDelegation` verbatim. It is the reserved second
|
|
91
|
+
* plus one millisecond, because CDP refuses a bare `.000Z` (401 `unauthorized` "Wallet authentication
|
|
92
|
+
* error" โ isolated on one session: `.000Z` refused, `.137Z` accepted, `.000Z` refused again) and
|
|
93
|
+
* truncates the filler away, storing exactly the reserved second.
|
|
94
|
+
*
|
|
95
|
+
* CONFIRM โ the server re-reads the grant from CDP and refuses unless the instant CDP reports IS the
|
|
96
|
+
* reserved one. That is the assertion the whole construction rests on: if CDP ever normalises,
|
|
97
|
+
* truncates differently, or another actor's grant is what reads back, this is where it surfaces
|
|
98
|
+
* instead of silently binding a permission to a row it was never matched to.
|
|
99
|
+
*
|
|
100
|
+
* ๐ด A CONFIRM MISMATCH STAYS UNCONFIRMED AND REQUIRES AN EXPLICIT STOP. CDP exposes revoke by user id,
|
|
101
|
+
* not by grant identity. A compensating revoke after this function's grant read could therefore delete a
|
|
102
|
+
* newer permission that replaced the one observed here. The durable cap never becomes active, so Otto
|
|
103
|
+
* submits nothing under the mismatched grant; the person uses the normal Stop action to revoke whatever
|
|
104
|
+
* grant is current under a fresh read.
|
|
105
|
+
*
|
|
106
|
+
* ๐ด NEITHER PHASE TAKES A USER ID OR AN EXPIRY FROM THE CALLER. The identity comes from
|
|
107
|
+
* `validateAccessToken`; the reserved instant comes from the store; the live instant comes from CDP.
|
|
108
|
+
*/
|
|
109
|
+
export declare function reserveDelegationCap(runtime: ExecutionToolRuntime, rawInput: unknown, caller: DelegationCaller): Promise<DelegationCapResult>;
|
|
64
110
|
/** What the support path writes to the structured log: the user id and the REDACTED reason โ never a token. */
|
|
65
111
|
export interface SupportActionSink {
|
|
66
112
|
write(entry: {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* execution-delegation-admin.ts โ the
|
|
2
|
+
* execution-delegation-admin.ts โ the four delegation ADMIN tools on the hosted MCP (PR-1b, seat ruling D10 = (a)).
|
|
3
3
|
*
|
|
4
4
|
* otto_delegation_fence โ the ONLY writer of the Model-B project policy: ensure-by-digest + read-back.
|
|
5
5
|
* otto_delegation_status โ one user's grant/fence read-back, bound to the user a CDP access token resolves to.
|
|
6
6
|
* otto_delegation_revoke โ developer-side revoke with read-back, fail-closed under the rig lock.
|
|
7
|
+
* otto_delegation_cap โ reserves the expiry a delegation is minted with, records the chosen per-swap
|
|
8
|
+
* cap against it, and confirms after the mint that CDP kept that exact instant.
|
|
7
9
|
*
|
|
8
10
|
* Every tool: the request header FIRST (constant-time, before parsing or any CDP read โ the same seam as
|
|
9
11
|
* `otto_submit_under_delegation`), then the strict schema, then the reads. Nothing here logs a token, the
|
|
@@ -47,9 +49,10 @@
|
|
|
47
49
|
import { getAddress } from 'viem';
|
|
48
50
|
import { revocationRequested } from './adapter/cdp-signer.js';
|
|
49
51
|
import { redactBootMessage } from './boot-redaction.js';
|
|
50
|
-
import { DELEGATION_FENCE_INPUT_SCHEMA, DELEGATION_REVOKE_INPUT_SCHEMA, DELEGATION_STATUS_INPUT_SCHEMA, } from './execution-delegation-admin-definition.js';
|
|
52
|
+
import { DELEGATION_CAP_INPUT_SCHEMA, DELEGATION_FENCE_INPUT_SCHEMA, DELEGATION_REVOKE_INPUT_SCHEMA, DELEGATION_STATUS_INPUT_SCHEMA, } from './execution-delegation-admin-definition.js';
|
|
51
53
|
import { assertDelegationCaller, } from './execution-delegation.js';
|
|
52
|
-
import { MODEL_B_PER_SWAP_CAP_USD, MODEL_B_POLICY_LINEAGE_PREFIX,
|
|
54
|
+
import { boundedDelegationExpiry, MODEL_B_PER_SWAP_CAP_MIN_USD, MODEL_B_PER_SWAP_CAP_USD, MODEL_B_POLICY_LINEAGE_PREFIX, MODEL_B_POLICY_NAME, MODEL_B_POLICY_VERSION, fenceIsCurrent, modelBLineageVersion, modelBProjectPolicyBody, policyRulesDigest, readBackModelBFence, } from './execution-delegation-policy.js';
|
|
55
|
+
import { DelegationCapExpiryUnavailableError, DELEGATION_MINT_PAUSED_MESSAGE, DELEGATION_TRADE_MINT_RETIRED_MESSAGE, effectiveTradeMintEnabled, MODEL_B_CAP_RULESET_VERSION, } from './execution-delegation-cap-store.js';
|
|
53
56
|
import { refuse } from './execution-errors.js';
|
|
54
57
|
const DEFAULT_FENCE_WAIT = { pollMs: 500, maxPolls: 20 };
|
|
55
58
|
function servicesOf(runtime) {
|
|
@@ -58,6 +61,10 @@ function servicesOf(runtime) {
|
|
|
58
61
|
return refuse('DELEGATION_UNAVAILABLE', 'delegated send is not configured on this server');
|
|
59
62
|
return services;
|
|
60
63
|
}
|
|
64
|
+
function exactMintClaim(claim, userId, mintKey, generation) {
|
|
65
|
+
return claim?.userId === userId && claim.kind === 'mint' && claim.claimId === mintKey &&
|
|
66
|
+
claim.claimGeneration === generation;
|
|
67
|
+
}
|
|
61
68
|
function parseWith(schema, rawInput) {
|
|
62
69
|
const parsed = schema.safeParse(rawInput);
|
|
63
70
|
if (!parsed.success) {
|
|
@@ -110,7 +117,9 @@ export async function ensureDelegationFence(runtime, rawInput, caller, wait = {}
|
|
|
110
117
|
if (project.length > 1)
|
|
111
118
|
return refuseAmbiguous(project);
|
|
112
119
|
const before = readBackModelBFence(snapshot);
|
|
113
|
-
|
|
120
|
+
// A v1 fence reads back as PRESENT (swaps and deposits keep working under it) but is not current: it
|
|
121
|
+
// is upgraded in place below โ the one write the Founder's `fence v2` word triggers, by running this tool.
|
|
122
|
+
if (before.present && fenceIsCurrent(before))
|
|
114
123
|
return fenceResult('unchanged', before.id, before.rulesDigest);
|
|
115
124
|
let action;
|
|
116
125
|
let targetId;
|
|
@@ -129,8 +138,12 @@ export async function ensureDelegationFence(runtime, rawInput, caller, wait = {}
|
|
|
129
138
|
if (againProject.length > 1)
|
|
130
139
|
return refuseAmbiguous(againProject);
|
|
131
140
|
const read = readBackModelBFence(again);
|
|
132
|
-
|
|
141
|
+
// Only a CURRENT fence is "unchanged" (codex gate r1 MEDIUM): a v1 fence that appeared during the
|
|
142
|
+
// ensure is present but not current, and this path never updates โ it says so and asks for a re-run.
|
|
143
|
+
if (read.present && fenceIsCurrent(read))
|
|
133
144
|
return fenceResult('unchanged', read.id, read.rulesDigest);
|
|
145
|
+
if (read.present)
|
|
146
|
+
return refuse('DELEGATION_FENCE_ABSENT', `a v${read.version} fence of our lineage appeared during the ensure (id ${read.id}); this path never updates โ run the fence tool again to upgrade it in place`);
|
|
134
147
|
const appeared = againProject[0];
|
|
135
148
|
if (!appeared)
|
|
136
149
|
return refuse('DELEGATION_FENCE_ABSENT', 'the create was refused as a duplicate but no project policy reads back; run the fence tool again');
|
|
@@ -175,7 +188,7 @@ export async function ensureDelegationFence(runtime, rawInput, caller, wait = {}
|
|
|
175
188
|
if (afterProject.length > 1)
|
|
176
189
|
return refuseAmbiguous(afterProject);
|
|
177
190
|
const after = readBackModelBFence(listed);
|
|
178
|
-
if (after.present && after.id === targetId)
|
|
191
|
+
if (after.present && fenceIsCurrent(after) && after.id === targetId)
|
|
179
192
|
return fenceResult(action, after.id, after.rulesDigest);
|
|
180
193
|
last = after.present
|
|
181
194
|
? `a v-current policy reads back under id ${after.id}, not the written id ${targetId}`
|
|
@@ -183,7 +196,7 @@ export async function ensureDelegationFence(runtime, rawInput, caller, wait = {}
|
|
|
183
196
|
if (poll + 1 < maxPolls)
|
|
184
197
|
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
185
198
|
}
|
|
186
|
-
return refuse('DELEGATION_FENCE_ABSENT', `the fence was ${action} (id ${targetId}) but does not read back as "${
|
|
199
|
+
return refuse('DELEGATION_FENCE_ABSENT', `the fence was ${action} (id ${targetId}) but does not read back as "${MODEL_B_POLICY_NAME}" with the v${MODEL_B_POLICY_VERSION} rules digest under that id after ${maxPolls} polls (${last}); nothing is submittable until it does`);
|
|
187
200
|
});
|
|
188
201
|
}
|
|
189
202
|
function fenceResult(action, id, rulesDigest) {
|
|
@@ -191,7 +204,7 @@ function fenceResult(action, id, rulesDigest) {
|
|
|
191
204
|
status: 'present',
|
|
192
205
|
action,
|
|
193
206
|
policy_id: id,
|
|
194
|
-
policy_name:
|
|
207
|
+
policy_name: MODEL_B_POLICY_NAME,
|
|
195
208
|
rules_digest: rulesDigest,
|
|
196
209
|
per_swap_cap_usd: MODEL_B_PER_SWAP_CAP_USD,
|
|
197
210
|
});
|
|
@@ -205,11 +218,21 @@ export async function readDelegationFenceStatus(runtime) {
|
|
|
205
218
|
const fence = readBackModelBFence(await services.rig.listProjectPolicies());
|
|
206
219
|
return Object.freeze({
|
|
207
220
|
present: fence.present,
|
|
208
|
-
|
|
221
|
+
// The name that READS BACK (v1 until the fence tool upgrades it), so the page can tell which fence stands.
|
|
222
|
+
policy_name: fence.present ? fence.name : MODEL_B_POLICY_NAME,
|
|
209
223
|
per_swap_cap_usd: MODEL_B_PER_SWAP_CAP_USD,
|
|
224
|
+
per_swap_cap_min_usd: MODEL_B_PER_SWAP_CAP_MIN_USD,
|
|
210
225
|
// The public authority binding: published present or absent, so the agent CLI mints to THIS project
|
|
211
226
|
// (and can still bind status/revoke to it after the fence is removed). Registration requires it set.
|
|
212
227
|
...(services.publicProjectId ? { project_id: services.publicProjectId } : {}),
|
|
228
|
+
ruleset_version: MODEL_B_CAP_RULESET_VERSION,
|
|
229
|
+
/**
|
|
230
|
+
* ๐ด THE PAUSE IS PUBLISHED, so the page and the CLI can SAY it before a person signs in rather than
|
|
231
|
+
* discovering it at the reserve. False means no new permission can be created on this worker; it says
|
|
232
|
+
* nothing about permissions that already stand โ those keep working, and revoke always does.
|
|
233
|
+
*/
|
|
234
|
+
// This public field describes Model B reserve, not Muse's separate global mint gate.
|
|
235
|
+
mint_enabled: effectiveTradeMintEnabled(services),
|
|
213
236
|
...(fence.present ? { rules_digest: fence.rulesDigest } : { reason: fence.reason }),
|
|
214
237
|
});
|
|
215
238
|
}
|
|
@@ -232,24 +255,345 @@ export async function readDelegationStatus(runtime, rawInput, caller) {
|
|
|
232
255
|
const input = parseWith(DELEGATION_STATUS_INPUT_SCHEMA, rawInput);
|
|
233
256
|
const user = await userFromAccessToken(services, input.accessToken);
|
|
234
257
|
const fence = readBackModelBFence(await services.rig.listProjectPolicies());
|
|
235
|
-
const requestedHere = revocationRequested(services.rig.rig, user.userId);
|
|
236
258
|
const grant = await services.rig.rig.readDelegation(user.userId);
|
|
237
259
|
const expiresAtMs = grant ? Date.parse(grant.expiresAt) : Number.NaN;
|
|
260
|
+
// Per GRANT, not per user: a revoke aimed at a permission the person no longer holds says nothing
|
|
261
|
+
// about the one they hold now.
|
|
262
|
+
const requestedHere = grant !== undefined && revocationRequested(services.rig.rig, user.userId, grant.expiresAt);
|
|
238
263
|
const active = grant !== undefined &&
|
|
239
264
|
!requestedHere &&
|
|
240
265
|
Number.isFinite(expiresAtMs) &&
|
|
241
266
|
expiresAtMs > runtime.nowMs();
|
|
267
|
+
/**
|
|
268
|
+
* ๐ด THE CAP COMES FROM THE CONFIRMED-ACTIVE RECORD, AND ONLY WHEN IT NAMES THE GRANT THAT READS BACK
|
|
269
|
+
* โ exactly the check the delegated send makes, so this surface and the send can never disagree about
|
|
270
|
+
* what is in force. A reservation that was never confirmed shows NO cap, because it authorises
|
|
271
|
+
* nothing. It is OMITTED, never defaulted: absence here means the send will refuse, and a page that
|
|
272
|
+
* showed the ceiling instead would tell the reader their permission allows $5,000 when it allows
|
|
273
|
+
* nothing at all.
|
|
274
|
+
*/
|
|
275
|
+
let recordedCapUsd;
|
|
276
|
+
if (grant && services.capStore.available) {
|
|
277
|
+
try {
|
|
278
|
+
const confirmed = await services.capStore.readActive(user.userId, runtime.nowMs());
|
|
279
|
+
if (confirmed &&
|
|
280
|
+
Date.parse(confirmed.expiresAt) === expiresAtMs &&
|
|
281
|
+
confirmed.requiredVersion <= MODEL_B_CAP_RULESET_VERSION) {
|
|
282
|
+
recordedCapUsd = confirmed.perSwapCapUsd;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
// A read that FAILED is not "nothing is confirmed" โ the send will refuse either way, and this
|
|
287
|
+
// surface says so by omitting the field rather than inventing a number in either direction.
|
|
288
|
+
recordedCapUsd = undefined;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
242
291
|
return Object.freeze({
|
|
243
292
|
user_id: user.userId,
|
|
244
293
|
addresses: [...user.addresses],
|
|
245
294
|
fence: Object.freeze({
|
|
246
295
|
present: fence.present,
|
|
247
|
-
policy_name:
|
|
296
|
+
policy_name: fence.present ? fence.name : MODEL_B_POLICY_NAME,
|
|
248
297
|
per_swap_cap_usd: MODEL_B_PER_SWAP_CAP_USD,
|
|
249
298
|
...(fence.present ? { rules_digest: fence.rulesDigest } : {}),
|
|
250
299
|
}),
|
|
251
|
-
delegation: Object.freeze({
|
|
300
|
+
delegation: Object.freeze({
|
|
301
|
+
active,
|
|
302
|
+
...(grant ? { expires_at: grant.expiresAt } : {}),
|
|
303
|
+
...(recordedCapUsd !== undefined ? { per_swap_cap_usd: recordedCapUsd } : {}),
|
|
304
|
+
}),
|
|
252
305
|
revocation_requested_here: requestedHere,
|
|
306
|
+
// What this worker implements โ the page and the CLI refuse to reserve or mint against an older one.
|
|
307
|
+
ruleset_version: MODEL_B_CAP_RULESET_VERSION,
|
|
308
|
+
// Whether a NEW permission can be created here at all (the staged-deploy gate). Never gates this read,
|
|
309
|
+
// and never gates revoke.
|
|
310
|
+
// Older dApp clients check this immediately before replacement revoke. Keep it false
|
|
311
|
+
// whenever the server will refuse Model B reserve, so they stop before revoking.
|
|
312
|
+
mint_enabled: effectiveTradeMintEnabled(services),
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* `otto_delegation_cap` โ RESERVE the expiry a delegation will be minted with, record the chosen per-swap
|
|
317
|
+
* cap against it, and CONFIRM the mint (Founder word `delegate cap choice`, 2026-09-02; round 3, after the
|
|
318
|
+
* codex gate NO-GO'd rounds 1 and 2 and the seat ruled the collision out rather than documenting it).
|
|
319
|
+
*
|
|
320
|
+
* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
321
|
+
* ๐ด WHY THE ORDER IS RESERVE โ MINT โ CONFIRM, AND NOT MINT โ RECORD
|
|
322
|
+
* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
323
|
+
* The gate's second finding is the whole reason: an expiry is the ONLY thing a send can learn about a live
|
|
324
|
+
* grant, so keying the RECORD by anything else โ r1's expiry-as-key, r2's mint key โ fixes the record and
|
|
325
|
+
* not the send. Its probe minted a $10 replacement carrying the SAME expiry as a standing $5,000 row and
|
|
326
|
+
* watched a plan run at $5,000. Live confirmation of exactly that, on the dev project (2026-09-02T22:17Z):
|
|
327
|
+
* CDP refuses a second mint over a live grant (409 `already_exists`), but after a user-side revoke a
|
|
328
|
+
* re-mint at the SAME instant succeeds and reads back at that identical instant. Two grants for one user
|
|
329
|
+
* CAN share an expiry.
|
|
330
|
+
*
|
|
331
|
+
* So round 3 stops trying to tell them apart and makes them impossible:
|
|
332
|
+
*
|
|
333
|
+
* RESERVE โ the SERVER picks the instant. It walks back from the second the person's chosen duration
|
|
334
|
+
* lands on and takes the first whole second no durable row for this user occupies, then writes the cap
|
|
335
|
+
* against it BEFORE any grant exists. The caller cannot choose the enforcement key, which is the
|
|
336
|
+
* property that makes it unique. The ladder only ever steps EARLIER (seat ruling): a permission must
|
|
337
|
+
* never outlive the duration the person chose, and the page shows the instant it actually got.
|
|
338
|
+
*
|
|
339
|
+
* MINT โ the caller passes `mint_expires_at` to `createDelegation` verbatim. It is the reserved second
|
|
340
|
+
* plus one millisecond, because CDP refuses a bare `.000Z` (401 `unauthorized` "Wallet authentication
|
|
341
|
+
* error" โ isolated on one session: `.000Z` refused, `.137Z` accepted, `.000Z` refused again) and
|
|
342
|
+
* truncates the filler away, storing exactly the reserved second.
|
|
343
|
+
*
|
|
344
|
+
* CONFIRM โ the server re-reads the grant from CDP and refuses unless the instant CDP reports IS the
|
|
345
|
+
* reserved one. That is the assertion the whole construction rests on: if CDP ever normalises,
|
|
346
|
+
* truncates differently, or another actor's grant is what reads back, this is where it surfaces
|
|
347
|
+
* instead of silently binding a permission to a row it was never matched to.
|
|
348
|
+
*
|
|
349
|
+
* ๐ด A CONFIRM MISMATCH STAYS UNCONFIRMED AND REQUIRES AN EXPLICIT STOP. CDP exposes revoke by user id,
|
|
350
|
+
* not by grant identity. A compensating revoke after this function's grant read could therefore delete a
|
|
351
|
+
* newer permission that replaced the one observed here. The durable cap never becomes active, so Otto
|
|
352
|
+
* submits nothing under the mismatched grant; the person uses the normal Stop action to revoke whatever
|
|
353
|
+
* grant is current under a fresh read.
|
|
354
|
+
*
|
|
355
|
+
* ๐ด NEITHER PHASE TAKES A USER ID OR AN EXPIRY FROM THE CALLER. The identity comes from
|
|
356
|
+
* `validateAccessToken`; the reserved instant comes from the store; the live instant comes from CDP.
|
|
357
|
+
*/
|
|
358
|
+
export async function reserveDelegationCap(runtime, rawInput, caller) {
|
|
359
|
+
const services = servicesOf(runtime);
|
|
360
|
+
assertDelegationCaller(services, caller);
|
|
361
|
+
const input = parseWith(DELEGATION_CAP_INPUT_SCHEMA, rawInput);
|
|
362
|
+
// No new Model B mint claim or reservation is issued. Confirm remains available for a
|
|
363
|
+
// permission already minted against a reservation before this release.
|
|
364
|
+
if (input.phase === 'reserve' && !services.tradeMintEnabled) {
|
|
365
|
+
return refuse('DELEGATION_MINT_PAUSED', DELEGATION_TRADE_MINT_RETIRED_MESSAGE);
|
|
366
|
+
}
|
|
367
|
+
if (!services.capStore.available) {
|
|
368
|
+
return refuse('DELEGATION_CAP_STORE_UNAVAILABLE', `the per-swap cap cannot be recorded on this server, so nothing would enforce it (${services.capStore.unavailableReason ?? 'store unavailable'})`);
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* ๐ด THE STAGED-DEPLOY GATE, ON RESERVE ONLY (gate finding, CRITICAL 2 โ the first rolling deployment).
|
|
372
|
+
*
|
|
373
|
+
* With `DELEGATION_MINT_ENABLED` false โ the default in code โ no NEW permission can be created on
|
|
374
|
+
* this worker, so there is no cap-bearing grant a still-serving cap-unaware binary could execute at
|
|
375
|
+
* the shared ceiling. That is the fence the ruleset handshake cannot be for the FIRST rollout, because
|
|
376
|
+
* the binary that would have to honour it predates it.
|
|
377
|
+
*
|
|
378
|
+
* ๐ด AND CONFIRM IS DELIBERATELY NOT GATED. Confirm binds a permission that ALREADY EXISTS at Coinbase
|
|
379
|
+
* to the reservation it was minted for. Refusing it would leave a live grant with no record โ which is
|
|
380
|
+
* fail-closed for sending, but strictly worse for the person: an unbindable permission they have to
|
|
381
|
+
* find and revoke. The flag is flipped falseโtrue by the runbook, so the only mid-flow flip is
|
|
382
|
+
* onโoff, and letting an in-flight mint finish binding is the safe half of that.
|
|
383
|
+
*
|
|
384
|
+
* Sends under permissions that already stand are untouched, and REVOKE is never gated: pausing the
|
|
385
|
+
* creation of authority must never pause its withdrawal.
|
|
386
|
+
*/
|
|
387
|
+
if (input.phase === 'reserve' && !services.mintEnabled) {
|
|
388
|
+
return refuse('DELEGATION_MINT_PAUSED', DELEGATION_MINT_PAUSED_MESSAGE);
|
|
389
|
+
}
|
|
390
|
+
const user = await userFromAccessToken(services, input.accessToken);
|
|
391
|
+
return input.phase === 'reserve'
|
|
392
|
+
? reservePhase(services, runtime, user.userId, input)
|
|
393
|
+
: confirmPhase(services, runtime, user.userId, input);
|
|
394
|
+
}
|
|
395
|
+
/** The RESERVE phase: claim a free instant, record the cap against it, hand back what to mint with. */
|
|
396
|
+
async function reservePhase(services, runtime, userId, input) {
|
|
397
|
+
const nowMs = runtime.nowMs();
|
|
398
|
+
if (!services.operationStore.available) {
|
|
399
|
+
return refuse('DELEGATION_OPERATION_STORE_UNAVAILABLE', 'the unified operation journal is unavailable; no permission is reserved');
|
|
400
|
+
}
|
|
401
|
+
let journalReady;
|
|
402
|
+
try {
|
|
403
|
+
journalReady = (await services.operationStore.journalStatus()).ready;
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
return refuse('DELEGATION_OPERATION_STORE_UNAVAILABLE', 'the unified operation journal could not be read; no permission is reserved');
|
|
407
|
+
}
|
|
408
|
+
if (!journalReady) {
|
|
409
|
+
return refuse('DELEGATION_OPERATION_JOURNAL_NOT_READY', 'the unified operation journal is waiting for every legacy worker to drain; no permission is reserved');
|
|
410
|
+
}
|
|
411
|
+
if (!services.exitStore?.available) {
|
|
412
|
+
return refuse('DELEGATION_OPERATION_STORE_UNAVAILABLE', 'the unified mint/delegated/exit claim store is unavailable; no permission is reserved');
|
|
413
|
+
}
|
|
414
|
+
let claimGeneration;
|
|
415
|
+
try {
|
|
416
|
+
// Sample before the separate unified-claim write. `capStore.reserve` persists this exact value, so
|
|
417
|
+
// a Stop that lands anywhere afterwards makes the reservation permanently cancelable rather than
|
|
418
|
+
// letting a delayed cap transaction join the newer generation.
|
|
419
|
+
claimGeneration = await services.capStore.readRevokeGeneration(userId);
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
return refuse('DELEGATION_CAP_STORE_UNAVAILABLE', 'the current Stop generation could not be read durably; no permission is reserved');
|
|
423
|
+
}
|
|
424
|
+
try {
|
|
425
|
+
const claim = await services.exitStore.claimMint(userId, input.mintKey, nowMs, claimGeneration);
|
|
426
|
+
if (claim.outcome === 'conflict') {
|
|
427
|
+
return refuse('DELEGATION_OPERATION_UNRESOLVED', `this end user has an unresolved ${claim.claim.kind} operation (${claim.claim.claimId}); no permission is reserved`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
return refuse('DELEGATION_OPERATION_STORE_UNAVAILABLE', 'the permission could not acquire the unified execution claim; no permission is reserved');
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* The LATEST instant this permission may carry โ the person's own duration, on a whole second. The
|
|
435
|
+
* ladder never returns anything after it, so an offset can only ever shorten a grant.
|
|
436
|
+
*/
|
|
437
|
+
let latest;
|
|
438
|
+
try {
|
|
439
|
+
latest = boundedDelegationExpiry(input.requestedDays, nowMs);
|
|
440
|
+
}
|
|
441
|
+
catch (error) {
|
|
442
|
+
return refuse('DELEGATION_INPUT_INVALID', error instanceof Error ? error.message : 'the requested expiry is not usable');
|
|
443
|
+
}
|
|
444
|
+
let reservation;
|
|
445
|
+
try {
|
|
446
|
+
reservation = await services.capStore.reserve(userId, input.mintKey, latest.expiresAt, input.perSwapCapUsd, nowMs, claimGeneration);
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
// Keep the cross-kind claim even when the caps write fails. Another same-key process may already
|
|
450
|
+
// have observed it and still commit its reservation; only a successful confirm or explicit Stop can
|
|
451
|
+
// prove every issued mint instruction harmless enough to release the shared claim.
|
|
452
|
+
if (error instanceof DelegationCapExpiryUnavailableError) {
|
|
453
|
+
return refuse('DELEGATION_CAP_EXPIRY_UNAVAILABLE', 'no free expiry instant is available for this delegation right now; nothing is reserved โ try again in a moment');
|
|
454
|
+
}
|
|
455
|
+
// The claim (or the record beside it) failed. Nothing is cached either โ the store writes disk
|
|
456
|
+
// first โ so the honest answer is that nothing was reserved, never a success the caller mints against.
|
|
457
|
+
return refuse('DELEGATION_CAP_STORE_UNAVAILABLE', 'the per-swap cap could not be written durably; nothing is reserved and no permission should be minted');
|
|
458
|
+
}
|
|
459
|
+
let claimAfterReserve;
|
|
460
|
+
let generationAfterReserve;
|
|
461
|
+
try {
|
|
462
|
+
[claimAfterReserve, generationAfterReserve] = await Promise.all([
|
|
463
|
+
services.exitStore.readClaim(userId),
|
|
464
|
+
services.capStore.readRevokeGeneration(userId),
|
|
465
|
+
]);
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
// Ambiguous revalidation retains the claim. That blocks every conflicting financial action until a
|
|
469
|
+
// later exact confirm or Stop can resolve it.
|
|
470
|
+
return refuse('DELEGATION_OPERATION_STORE_UNAVAILABLE', 'the permission claim could not be revalidated after reservation; nothing should be minted');
|
|
471
|
+
}
|
|
472
|
+
if (reservation.record.revokeGeneration !== claimGeneration ||
|
|
473
|
+
generationAfterReserve !== claimGeneration ||
|
|
474
|
+
!exactMintClaim(claimAfterReserve, userId, input.mintKey, claimGeneration)) {
|
|
475
|
+
// The row, if written, carries the older generation and confirm will classify it as cancelled.
|
|
476
|
+
// Delete only this exact claim; a later same-key claim under a newer generation is untouched.
|
|
477
|
+
await services.exitStore.releaseMint(userId, input.mintKey, claimGeneration).catch(() => false);
|
|
478
|
+
return refuse('DELEGATION_CAP_CANCELLED', 'Stop overlapped this permission reservation; nothing is confirmed. Check status and use Stop if a permission remains active');
|
|
479
|
+
}
|
|
480
|
+
return capResult('reserve', reservation.record, reservation.outcome === 'reserved');
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* The CONFIRM phase: the instant CDP holds must BE the instant that was reserved, and the reservation
|
|
484
|
+
* must still be confirmable. Promotes it to confirmed-active in one durable write. A mismatch stays
|
|
485
|
+
* unconfirmed and requires an explicit Stop because CDP exposes only a user-wide revoke.
|
|
486
|
+
*/
|
|
487
|
+
async function confirmPhase(services, runtime, userId, input) {
|
|
488
|
+
const nowMs = runtime.nowMs();
|
|
489
|
+
if (!services.exitStore.available) {
|
|
490
|
+
return refuse('DELEGATION_OPERATION_STORE_UNAVAILABLE', 'the unified mint/delegated/exit claim store is unavailable; nothing is confirmed');
|
|
491
|
+
}
|
|
492
|
+
let reservation;
|
|
493
|
+
try {
|
|
494
|
+
reservation = await services.capStore.readReservation(userId, input.mintKey, nowMs);
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
return refuse('DELEGATION_CAP_STORE_UNAVAILABLE', 'the reservation for this mint could not be read; nothing is confirmed');
|
|
498
|
+
}
|
|
499
|
+
if (!reservation) {
|
|
500
|
+
return refuse('DELEGATION_CAP_UNRECORDED', 'no per-swap cap was reserved for this mint, so nothing is confirmed. If a permission reads back, use Stop to revoke it; new Model B setup is retired');
|
|
501
|
+
}
|
|
502
|
+
const claimGeneration = reservation.revokeGeneration;
|
|
503
|
+
let claim;
|
|
504
|
+
try {
|
|
505
|
+
claim = await services.exitStore.readClaim(userId);
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
return refuse('DELEGATION_OPERATION_STORE_UNAVAILABLE', 'the permission claim could not be read; nothing is confirmed');
|
|
509
|
+
}
|
|
510
|
+
if (!exactMintClaim(claim, userId, input.mintKey, claimGeneration)) {
|
|
511
|
+
if (claim) {
|
|
512
|
+
return refuse('DELEGATION_OPERATION_UNRESOLVED', `this end user has an unresolved ${claim.kind} operation (${claim.claimId}); nothing is confirmed`);
|
|
513
|
+
}
|
|
514
|
+
return refuse('DELEGATION_CAP_CANCELLED', 'the exact generation-bound permission claim no longer stands; nothing is confirmed. Check status and use Stop if a permission remains active');
|
|
515
|
+
}
|
|
516
|
+
const grant = await services.rig.rig.readDelegation(userId);
|
|
517
|
+
const grantMs = grant ? Date.parse(grant.expiresAt) : Number.NaN;
|
|
518
|
+
if (!grant ||
|
|
519
|
+
!Number.isFinite(grantMs) ||
|
|
520
|
+
grantMs <= nowMs ||
|
|
521
|
+
revocationRequested(services.rig.rig, userId, grant.expiresAt)) {
|
|
522
|
+
return refuse('DELEGATION_NOT_ACTIVE', 'no active delegation reads back for this end user, so there is nothing to confirm');
|
|
523
|
+
}
|
|
524
|
+
let claimAfterGrant;
|
|
525
|
+
try {
|
|
526
|
+
claimAfterGrant = await services.exitStore.readClaim(userId);
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
return refuse('DELEGATION_OPERATION_STORE_UNAVAILABLE', 'the permission claim could not be revalidated after the grant read; nothing is confirmed');
|
|
530
|
+
}
|
|
531
|
+
if (!exactMintClaim(claimAfterGrant, userId, input.mintKey, claimGeneration)) {
|
|
532
|
+
return refuseCancelledMint();
|
|
533
|
+
}
|
|
534
|
+
let outcome;
|
|
535
|
+
try {
|
|
536
|
+
outcome = await services.capStore.confirm(userId, input.mintKey, grant.expiresAt, nowMs);
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
return refuse('DELEGATION_CAP_STORE_UNAVAILABLE', 'the reservation for this mint could not be read or promoted, so nothing is confirmed');
|
|
540
|
+
}
|
|
541
|
+
switch (outcome.outcome) {
|
|
542
|
+
case 'confirmed':
|
|
543
|
+
case 'already_confirmed':
|
|
544
|
+
await services.exitStore.releaseMint(userId, input.mintKey, claimGeneration);
|
|
545
|
+
return capResult('confirm', outcome.record, outcome.outcome === 'confirmed');
|
|
546
|
+
case 'unreserved':
|
|
547
|
+
return refuse('DELEGATION_CAP_UNRECORDED', 'no per-swap cap was reserved for this mint, so nothing is confirmed. If a permission reads back, use Stop to revoke it; new Model B setup is retired');
|
|
548
|
+
case 'not_reservable':
|
|
549
|
+
return refuse('DELEGATION_CAP_UNRECORDED', `the reservation for this mint is ${outcome.state} and can no longer be confirmed. Check status and use Stop if a permission remains active`);
|
|
550
|
+
case 'superseded':
|
|
551
|
+
/**
|
|
552
|
+
* ๐ด NEVER REVIVED. This reservation WAS confirmed and a later one replaced it; promoting it again
|
|
553
|
+
* would bring a replaced permission back, and on a wider cap that is the widening this whole unit
|
|
554
|
+
* exists to prevent.
|
|
555
|
+
*/
|
|
556
|
+
return refuse('DELEGATION_CAP_SUPERSEDED', 'this permission was replaced by a later one and is not brought back. Check the current permission in status; use Stop if you want to revoke it');
|
|
557
|
+
case 'cancelled':
|
|
558
|
+
/**
|
|
559
|
+
* ๐ด THE PERSON PRESSED STOP WHILE THIS PERMISSION WAS BEING CREATED (gate finding, r4: "Add a
|
|
560
|
+
* durable revoke generation so Stop cancels already-reserved/in-flight mints"). The reservation
|
|
561
|
+
* stays unconfirmed; an explicit fresh Stop safely revokes whichever remote grant is current.
|
|
562
|
+
*/
|
|
563
|
+
return refuseCancelledMint();
|
|
564
|
+
case 'expiry_mismatch':
|
|
565
|
+
return refuseMismatchedGrant();
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* A Stop or replacement landed between this mint's reservation and confirmation. The exact claim is no
|
|
570
|
+
* longer current, so this stale confirm must not call CDP's user-wide revoke: that call could revoke the
|
|
571
|
+
* replacement permission. The old reservation authorises nothing, and an explicit fresh Stop handles the
|
|
572
|
+
* current remote grant.
|
|
573
|
+
*/
|
|
574
|
+
function refuseCancelledMint() {
|
|
575
|
+
return refuse('DELEGATION_CAP_CANCELLED', 'this permission was stopped or replaced while it was being created; nothing is confirmed for it and no automatic user-wide revoke was issued. Check status and press Stop if you want to revoke the current permission');
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* The live permission is not the one this cap was reserved for. It remains unconfirmed and therefore
|
|
579
|
+
* authorises nothing through Otto. CDP has no conditional grant-id revoke, so this stale confirm never
|
|
580
|
+
* issues a user-wide compensating revoke that could take away a newer permission.
|
|
581
|
+
*/
|
|
582
|
+
function refuseMismatchedGrant() {
|
|
583
|
+
return refuse('DELEGATION_CAP_GRANT_CHANGED', 'the delegation that reads back is not the one this per-swap cap was reserved for; it remains unconfirmed and nothing is submitted under it. Check status and press Stop if you want to revoke the current permission');
|
|
584
|
+
}
|
|
585
|
+
/** One shape for both phases, so the two answers cannot drift apart. */
|
|
586
|
+
function capResult(phase, record, wroteIt) {
|
|
587
|
+
return Object.freeze({
|
|
588
|
+
phase,
|
|
589
|
+
user_id: record.userId,
|
|
590
|
+
expires_at: record.expiresAt,
|
|
591
|
+
// The value to mint with exists only on the reserve โ by confirm the mint has already happened.
|
|
592
|
+
...(phase === 'reserve' ? { mint_expires_at: record.requestedExpiresAt } : {}),
|
|
593
|
+
per_swap_cap_usd: record.perSwapCapUsd,
|
|
594
|
+
state: record.state,
|
|
595
|
+
ruleset_version: record.requiredVersion,
|
|
596
|
+
recorded: wroteIt,
|
|
253
597
|
});
|
|
254
598
|
}
|
|
255
599
|
const STDOUT_SUPPORT_SINK = {
|
|
@@ -277,6 +621,72 @@ export async function revokeDelegation(runtime, rawInput, caller, supportSink =
|
|
|
277
621
|
ts: new Date(runtime.nowMs()).toISOString(),
|
|
278
622
|
});
|
|
279
623
|
}
|
|
624
|
+
/**
|
|
625
|
+
* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
626
|
+
* ๐ด THE DURABLE RECORD IS REVOKED IN THE SAME OPERATION, AND FIRST (gate finding, round 4 CRITICAL 1)
|
|
627
|
+
* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
628
|
+
* "Revoke does not transition the durable record. The normal revoke calls only `rig.revoke()` and never
|
|
629
|
+
* `capStore.revoke()` โฆ a new grant can inherit the revoked grant's old $5,000 cap without
|
|
630
|
+
* confirmation. This is exactly the prohibited widening direction." Its probe on the shipped fixtures:
|
|
631
|
+
* revoke โ fresh process โ same-expiry re-mint โ `afterReportedRevoke=confirmed_active, sendStatus=sent,
|
|
632
|
+
* enforcedOldCap=5000`.
|
|
633
|
+
*
|
|
634
|
+
* The in-process revocation marker dies with the process; the RECORD does not. So the record moves
|
|
635
|
+
* first, and the CDP call happens only after it has:
|
|
636
|
+
*
|
|
637
|
+
* ยท the grant is read FIRST, so the record and the marker name the SAME permission;
|
|
638
|
+
* ยท a durable transition that THROWS means nothing is revoked at Coinbase either โ the caller is told
|
|
639
|
+
* to retry, rather than being left with a removed grant and a record that still says it stands;
|
|
640
|
+
* ยท a transition that finds nothing to change (`false`) is NOT a failure: the record was already
|
|
641
|
+
* revoked, or this person's live grant was minted out-of-model and was never recorded here. The
|
|
642
|
+
* CDP revoke still goes out โ a person must always be able to take a permission away;
|
|
643
|
+
* ยท if the CDP revoke then fails, the record is ALREADY revoked. That is the safe direction (this
|
|
644
|
+
* server will submit nothing under it) and a retry re-issues the CDP call.
|
|
645
|
+
*
|
|
646
|
+
* ๐ด THE EXPIRY IT CONSUMED STAYS CONSUMED. The reservation row survives as `revoked`, so no later
|
|
647
|
+
* permission minted through Otto's page or CLI can land on that instant โ which is what closes the
|
|
648
|
+
* probe above: the re-minted grant gets a DIFFERENT second, matches no confirmed record, and is
|
|
649
|
+
* refused rather than inheriting anything.
|
|
650
|
+
*/
|
|
651
|
+
let capturedMintClaim;
|
|
652
|
+
if (services.capStore.available && services.exitStore.available) {
|
|
653
|
+
try {
|
|
654
|
+
const claim = await services.exitStore.readClaim(userId);
|
|
655
|
+
if (claim?.kind === 'mint' && claim.claimGeneration !== undefined)
|
|
656
|
+
capturedMintClaim = claim;
|
|
657
|
+
}
|
|
658
|
+
catch {
|
|
659
|
+
// Stop and remote revoke remain available; an unreadable claim is conservatively retained.
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
let durableStopRecorded = false;
|
|
663
|
+
if (services.capStore.available) {
|
|
664
|
+
try {
|
|
665
|
+
/**
|
|
666
|
+
* ๐ด ONE TRANSACTION, BOTH WRITES (gate r5 CRITICAL: "commits the generation bump and then performs
|
|
667
|
+
* the active-record revoke in a separate transaction โฆ the process dies or stalls before
|
|
668
|
+
* `capStore.revoke()` โฆ a fresh worker still reads the old record as `confirmed_active`").
|
|
669
|
+
*
|
|
670
|
+
* `stop` bumps the revoke generation โ EVERY Stop, including one that finds no grant, the case the
|
|
671
|
+
* round-4 gate named, so a mint already in flight is cancelled at its confirm โ AND moves whatever
|
|
672
|
+
* record is confirmed-active to `revoked`, inside one `BEGIN IMMEDIATE`. A crash between the two
|
|
673
|
+
* commits neither. It takes no expiry: a Stop means Otto acts under nothing for this person, which
|
|
674
|
+
* is also what the CDP-side revoke below does to whatever grant is live, so the two halves agree
|
|
675
|
+
* without a read in between that could see a different grant than either of them acts on.
|
|
676
|
+
*/
|
|
677
|
+
await services.capStore.stop(userId, runtime.nowMs());
|
|
678
|
+
durableStopRecorded = true;
|
|
679
|
+
}
|
|
680
|
+
catch {
|
|
681
|
+
return refuse('DELEGATION_REVOKE_NOT_CONFIRMED', 'the permission could not be marked revoked durably, so nothing was revoked at Coinbase either โ retry');
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
// Only a successful durable generation bump may retire the exact mint claim observed before it. The
|
|
685
|
+
// mint key plus generation compare-delete cannot erase a newer claim, including same-key reuse after
|
|
686
|
+
// Stop. If the cap store is unavailable, retain the claim while still honoring remote revoke.
|
|
687
|
+
if (durableStopRecorded && capturedMintClaim?.claimGeneration !== undefined) {
|
|
688
|
+
await services.exitStore.releaseMint(userId, capturedMintClaim.claimId, capturedMintClaim.claimGeneration).catch(() => false);
|
|
689
|
+
}
|
|
280
690
|
try {
|
|
281
691
|
// Serialized on the rig, the lock held through the read-back; the user is marked revocation-requested BEFORE
|
|
282
692
|
// the raw revoke, so a plan queued behind this call is refused whatever the read-back says.
|