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
|
@@ -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,14 @@ 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 { confirmedDelegationOperationResult } from './execution-delegation-operations-definition.js';
|
|
56
|
+
import { MODEL_B_CAP_RULESET_VERSION } from './execution-delegation-cap-store.js';
|
|
57
|
+
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';
|
|
58
|
+
import { createDelegationCapStore, resolveDelegationCapStorePath, resolveDelegationMintEnabled, } from './execution-delegation-cap-store.js';
|
|
59
|
+
import { createDelegationOperationStore, delegationOperationIdempotencyKey, DelegationOperationJournalNotReadyError, } from './execution-delegation-operation-store.js';
|
|
60
|
+
import { verifyDelegatedStepFinality } from './execution-delegation-finality.js';
|
|
61
|
+
import { createDelegationExitStore, } from './execution-delegation-exit-store.js';
|
|
62
|
+
import { ExecutionWorkerBusyError, openExecutionWorkerGuardStore, unavailableExecutionWorkerGuardStore, } from './execution-worker-guard.js';
|
|
42
63
|
import { EXECUTION_REFUSAL_CODES, ExecutionRefusal, isExecutionRefusal, refuse, } from './execution-errors.js';
|
|
43
64
|
import { artifactCommitmentDigest, canonicalJson, } from './execution-types.js';
|
|
44
65
|
import { DELEGATED_SUBMISSION_GRACE_MS, preparedHere } from './prepared-artifacts.js';
|
|
@@ -129,12 +150,13 @@ function isAbsentDelegation(error) {
|
|
|
129
150
|
export function createDelegationRig(cdp, pub) {
|
|
130
151
|
const rig = createCdpRig({
|
|
131
152
|
pub,
|
|
132
|
-
submit: async (user, transaction) => {
|
|
153
|
+
submit: async (user, transaction, idempotencyKey) => {
|
|
133
154
|
const result = await cdp.endUser.sendEvmTransaction({
|
|
134
155
|
userId: user.userId,
|
|
135
156
|
address: user.address,
|
|
136
157
|
transaction,
|
|
137
158
|
network: 'base',
|
|
159
|
+
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
138
160
|
});
|
|
139
161
|
return result.transactionHash;
|
|
140
162
|
},
|
|
@@ -285,6 +307,58 @@ function requestFingerprint(input, commitment) {
|
|
|
285
307
|
.update(canonicalJson({ user_id: input.userId, commitment, prepare_input: prepareInput }))
|
|
286
308
|
.digest('hex');
|
|
287
309
|
}
|
|
310
|
+
function operationBinding(input, fingerprint) {
|
|
311
|
+
return Object.freeze({
|
|
312
|
+
artifactId: input.envelope.artifact_id,
|
|
313
|
+
userId: input.userId,
|
|
314
|
+
account: getAddress(input.prepareInput.accountProfile.address),
|
|
315
|
+
chainId: MODEL_B_CHAIN_ID,
|
|
316
|
+
fingerprint,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
function operationMatches(operation, binding) {
|
|
320
|
+
return (operation.artifactId === binding.artifactId &&
|
|
321
|
+
operation.userId === binding.userId &&
|
|
322
|
+
operation.account.toLowerCase() === binding.account.toLowerCase() &&
|
|
323
|
+
operation.chainId === binding.chainId &&
|
|
324
|
+
operation.fingerprint === binding.fingerprint);
|
|
325
|
+
}
|
|
326
|
+
/** The signer-facing adapter. Its first call commits the exact CDP request before returning the key. */
|
|
327
|
+
function operationJournal(store, binding, nowMs) {
|
|
328
|
+
return Object.freeze({
|
|
329
|
+
async prepare(attempt) {
|
|
330
|
+
const idempotencyKey = delegationOperationIdempotencyKey(binding.artifactId, attempt.index);
|
|
331
|
+
await store.prepareStep(binding, attempt.index, attempt.kind, {
|
|
332
|
+
userId: attempt.userId,
|
|
333
|
+
address: attempt.address,
|
|
334
|
+
transaction: attempt.transaction,
|
|
335
|
+
network: attempt.network,
|
|
336
|
+
idempotencyKey,
|
|
337
|
+
}, nowMs());
|
|
338
|
+
return Object.freeze({ idempotencyKey });
|
|
339
|
+
},
|
|
340
|
+
notSubmitted: (index, idempotencyKey) => store.markStepNotSubmitted(binding, index, idempotencyKey, nowMs()),
|
|
341
|
+
submitted: (index, idempotencyKey, transactionHash) => store.markStepSubmitted(binding, index, idempotencyKey, transactionHash, nowMs()),
|
|
342
|
+
unknown: (index, idempotencyKey) => store.markStepUnknown(binding, index, idempotencyKey, nowMs()),
|
|
343
|
+
receipt: (index, idempotencyKey, receipt) => store.markStepReceipt(binding, index, idempotencyKey, receipt, nowMs()),
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
function durableReplayOrRefusal(operation, binding) {
|
|
347
|
+
if (!operationMatches(operation, binding)) {
|
|
348
|
+
return refuse('DELEGATION_REPLAY_MISMATCH', 'this artifact is durably bound to a different request (user, account or intent); nothing is re-sent');
|
|
349
|
+
}
|
|
350
|
+
if (operation.state === 'confirmed') {
|
|
351
|
+
const confirmed = confirmedDelegationOperationResult(operation);
|
|
352
|
+
if (!confirmed) {
|
|
353
|
+
return refuse('DELEGATION_OPERATION_STORE_UNAVAILABLE', 'the confirmed delegated-operation result could not be read safely; nothing is re-sent');
|
|
354
|
+
}
|
|
355
|
+
return Object.freeze({ ...confirmed, replayed: true });
|
|
356
|
+
}
|
|
357
|
+
if (operation.state === 'failed') {
|
|
358
|
+
return refuse('DELEGATED_PLAN_HALTED', 'this artifact previously ended in a known failure; it is not retried — prepare again');
|
|
359
|
+
}
|
|
360
|
+
return refuse('DELEGATION_OPERATION_UNRESOLVED', `this artifact has a durable ${operation.state} operation whose final outcome is unresolved; nothing is re-sent`);
|
|
361
|
+
}
|
|
288
362
|
const SUBMISSIONS = new WeakMap();
|
|
289
363
|
function submissionsOf(services, nowMs) {
|
|
290
364
|
let records = SUBMISSIONS.get(services);
|
|
@@ -341,31 +415,39 @@ function parseInput(rawInput) {
|
|
|
341
415
|
}
|
|
342
416
|
return parsed.data;
|
|
343
417
|
}
|
|
344
|
-
function
|
|
418
|
+
function verifiedRecoveryContext(input, user, verified, fence, expiresAt) {
|
|
345
419
|
return Object.freeze({
|
|
346
420
|
status: 'sent',
|
|
347
421
|
user_id: user.userId,
|
|
348
422
|
address: user.address,
|
|
349
423
|
chain_id: MODEL_B_CHAIN_ID,
|
|
350
424
|
artifact_id: verified.artifact.artifact_id,
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
gas_used: step.gas_used,
|
|
359
|
-
})),
|
|
425
|
+
/**
|
|
426
|
+
* 🔴 TWO NUMBERS, BECAUSE THERE ARE TWO FACTS. `per_swap_cap_usd` is the ENGINE's — the constant
|
|
427
|
+
* compiled into the one project-scope policy, the same for every delegated user, and the only one
|
|
428
|
+
* Coinbase enforces. `user_cap_usd` is what THIS person chose when they minted, enforced by this
|
|
429
|
+
* server alone. Reporting only the second would describe the CDP fence as something it is not;
|
|
430
|
+
* reporting only the first would hide the number that actually bounded this send.
|
|
431
|
+
*/
|
|
360
432
|
fence: Object.freeze({
|
|
361
433
|
policy_id: fence.id,
|
|
362
|
-
policy_name:
|
|
434
|
+
policy_name: fence.name,
|
|
363
435
|
rules_digest: fence.rulesDigest,
|
|
364
436
|
per_swap_cap_usd: MODEL_B_PER_SWAP_CAP_USD,
|
|
437
|
+
user_cap_usd: fence.userCapUsd,
|
|
365
438
|
}),
|
|
366
439
|
delegation: Object.freeze({ expires_at: expiresAt }),
|
|
367
440
|
verification: Object.freeze({
|
|
368
|
-
|
|
441
|
+
/**
|
|
442
|
+
* The verifier's checks, plus the ROUTE the constructor recorded in the envelope (`route_tool:<lifi
|
|
443
|
+
* tool>` and `route_simulated:<true|false>` assertion ids), so the receipt says which venue executed.
|
|
444
|
+
* Carried as check strings because the receipt's wire shape is validated `.strict()` by the shipped
|
|
445
|
+
* dApp until cut #3 — a new field would break the page that is live today.
|
|
446
|
+
*/
|
|
447
|
+
checks: [
|
|
448
|
+
...verified.checks.map((check) => check.id),
|
|
449
|
+
...input.envelope.assertions.map((a) => a.id).filter((id) => id.startsWith('route_')),
|
|
450
|
+
],
|
|
369
451
|
gross: verified.gross.toString(),
|
|
370
452
|
min_amount_out: verified.minAmountOut.toString(),
|
|
371
453
|
otto_fee_atomic: verified.ottoFee.toString(),
|
|
@@ -375,6 +457,52 @@ function verifiedResult(input, user, verified, sent, fence, expiresAt) {
|
|
|
375
457
|
}),
|
|
376
458
|
});
|
|
377
459
|
}
|
|
460
|
+
function verifiedResult(input, user, verified, sent, fence, expiresAt) {
|
|
461
|
+
return Object.freeze({
|
|
462
|
+
...verifiedRecoveryContext(input, user, verified, fence, expiresAt),
|
|
463
|
+
steps: sent.map((step) => Object.freeze({
|
|
464
|
+
index: step.index,
|
|
465
|
+
kind: step.kind,
|
|
466
|
+
to: step.to,
|
|
467
|
+
transaction_hash: step.transaction_hash,
|
|
468
|
+
status: step.status,
|
|
469
|
+
block_number: step.block_number,
|
|
470
|
+
gas_used: step.gas_used,
|
|
471
|
+
})),
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* A live signer returns after sealed canonical inclusion so the next plan step can safely use the prior
|
|
476
|
+
* state. Releasing the cross-rail claim is stricter: every one of the three exact requests and receipts
|
|
477
|
+
* must also sit at or below the canonical finalized head. This bounded immediate read never waits for
|
|
478
|
+
* Base finality; `otto_delegation_reconcile` performs the same proof later.
|
|
479
|
+
*/
|
|
480
|
+
async function liveDelegatedSuccessIsFinalized(services, binding) {
|
|
481
|
+
let operation;
|
|
482
|
+
try {
|
|
483
|
+
operation = await services.operationStore.readRecoveryArtifact(binding.artifactId);
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
if (!operation)
|
|
489
|
+
return false;
|
|
490
|
+
if (!operationMatches(operation, binding)) {
|
|
491
|
+
return refuse('DELEGATION_REPLAY_MISMATCH', 'the durable delegated operation changed before finality could be checked; its claim remains unresolved');
|
|
492
|
+
}
|
|
493
|
+
const kinds = ['approve_reset', 'approve', 'call'];
|
|
494
|
+
if (operation.steps.length !== kinds.length ||
|
|
495
|
+
operation.steps.some((step, index) => step.index !== index || step.kind !== kinds[index] || step.state !== 'confirmed' ||
|
|
496
|
+
step.receipt?.status !== 'success' ||
|
|
497
|
+
step.receipt.transactionHash.toLowerCase() !== step.transactionHash?.toLowerCase()))
|
|
498
|
+
return false;
|
|
499
|
+
for (const step of operation.steps) {
|
|
500
|
+
const finality = await verifyDelegatedStepFinality(services.rig.rig.pub, operation, step);
|
|
501
|
+
if (finality.outcome !== 'verified')
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
378
506
|
/**
|
|
379
507
|
* Submit a prepared swap under the end user's delegation — the order in the file header is the contract.
|
|
380
508
|
* `caller` is derived from the request headers by the registration layer; it never comes from arguments.
|
|
@@ -387,13 +515,12 @@ export async function submitUnderDelegation(runtime, rawInput, caller, _signal)
|
|
|
387
515
|
assertDelegationCaller(services, caller);
|
|
388
516
|
const input = parseInput(rawInput);
|
|
389
517
|
const nowMs = runtime.nowMs();
|
|
390
|
-
// 1a. Only an artifact THIS process prepared — same id, same whole-envelope commitment — is submittable
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
//
|
|
396
|
-
// the record does (same clock value, same window), so a record can never exist without its entry.
|
|
518
|
+
// 1a. Only an artifact THIS process prepared — same id, same whole-envelope commitment — is submittable.
|
|
519
|
+
// The durable journal records and exposes outcomes across restart, but it is evidence, not authority for a
|
|
520
|
+
// replacement worker to adopt an envelope. Refused before any CDP read; the caller uses the authenticated
|
|
521
|
+
// operation lookup for recovery state and prepares again only when that state is terminal. The commitment
|
|
522
|
+
// is RECOMPUTED from the presented envelope (the field it carries is never trusted), so a tampered envelope
|
|
523
|
+
// under a known id is refused here, never replayed.
|
|
397
524
|
const artifactId = input.envelope.artifact_id;
|
|
398
525
|
// The strict schema has validated the shape; the digest is over whatever was presented (a malformed envelope
|
|
399
526
|
// digests to nothing this process recorded and is refused).
|
|
@@ -403,6 +530,7 @@ export async function submitUnderDelegation(runtime, rawInput, caller, _signal)
|
|
|
403
530
|
}
|
|
404
531
|
// 1b. Same artifact seen before? Replay its outcome — to the SAME request only; never send twice.
|
|
405
532
|
const fingerprint = requestFingerprint(input, commitment);
|
|
533
|
+
const binding = operationBinding(input, fingerprint);
|
|
406
534
|
const records = submissionsOf(services, nowMs);
|
|
407
535
|
const seen = records.get(artifactId);
|
|
408
536
|
if (seen) {
|
|
@@ -415,6 +543,24 @@ export async function submitUnderDelegation(runtime, rawInput, caller, _signal)
|
|
|
415
543
|
throw seen.state.refusal;
|
|
416
544
|
return refuse('DELEGATION_ARTIFACT_IN_FLIGHT', 'this artifact is being submitted right now; wait for its result');
|
|
417
545
|
}
|
|
546
|
+
if (!services.operationStore.available) {
|
|
547
|
+
return refuse('DELEGATION_OPERATION_STORE_UNAVAILABLE', `the durable delegated-operation journal is unavailable; nothing is submitted (${services.operationStore.unavailableReason ?? 'store unavailable'})`);
|
|
548
|
+
}
|
|
549
|
+
let durableSeen;
|
|
550
|
+
try {
|
|
551
|
+
durableSeen = await services.operationStore.readArtifact(artifactId);
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
return refuse('DELEGATION_OPERATION_STORE_UNAVAILABLE', 'the durable delegated-operation journal could not be read; nothing is submitted');
|
|
555
|
+
}
|
|
556
|
+
// Terminal replay needs no process-liveness proof and remains available even if old guard metadata was
|
|
557
|
+
// retired. An unresolved row must continue to the permanent-guard acquisition below; returning here
|
|
558
|
+
// would skip the missing/corrupt-guard refusal and could hide a broken recovery boundary.
|
|
559
|
+
if (durableSeen && (durableSeen.state === 'confirmed' || durableSeen.state === 'failed'))
|
|
560
|
+
return durableReplayOrRefusal(durableSeen, binding);
|
|
561
|
+
if (!services.exitStore?.available) {
|
|
562
|
+
return refuse('DELEGATION_OPERATION_STORE_UNAVAILABLE', 'the shared mint/delegated/exit claim store is unavailable or internally inconsistent; nothing is submitted');
|
|
563
|
+
}
|
|
418
564
|
const release = acquireUser(services, input.userId);
|
|
419
565
|
const submission = {
|
|
420
566
|
expiresAtMs: Date.parse(input.envelope.valid_until) + DELEGATED_SUBMISSION_GRACE_MS,
|
|
@@ -422,24 +568,39 @@ export async function submitUnderDelegation(runtime, rawInput, caller, _signal)
|
|
|
422
568
|
state: { kind: 'in_flight' },
|
|
423
569
|
};
|
|
424
570
|
records.set(artifactId, submission);
|
|
571
|
+
let durableStarted = false;
|
|
572
|
+
let allPlanStepsConfirmed = false;
|
|
573
|
+
// A live halt's allowance read-back is deliberately not release evidence: it is an unpinned RPC
|
|
574
|
+
// observation after a merely sealed clear receipt. Even when it reports zero, the clear may still be
|
|
575
|
+
// above finality or another backend may lag. Every halt therefore keeps the operation and unified
|
|
576
|
+
// claim until recovery revalidates all known receipts and reads zero at one exact finalized canonical
|
|
577
|
+
// block. This also keeps the release rule in one place.
|
|
578
|
+
let retainUnresolvedAfterHalt = false;
|
|
579
|
+
let durableWorkerGuard;
|
|
425
580
|
try {
|
|
426
581
|
// 2. FENCE before authority.
|
|
427
582
|
const fence = readBackModelBFence(await services.rig.listProjectPolicies());
|
|
428
583
|
if (!fence.present)
|
|
429
584
|
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
|
-
}
|
|
585
|
+
// 3. GRANT active past the envelope's own expiry; fail-closed once revocation was requested for IT.
|
|
434
586
|
const grant = await services.rig.rig.readDelegation(input.userId);
|
|
435
587
|
if (!grant)
|
|
436
588
|
return refuse('DELEGATION_NOT_ACTIVE', 'no active delegation reads back for this end user');
|
|
589
|
+
/**
|
|
590
|
+
* 🔴 PER GRANT, NOT PER USER (gate finding, HIGH — round 3). The marker used to be a sticky set of
|
|
591
|
+
* user ids, so once revoke was offered in every screen state a Stop press on a screen with no grant
|
|
592
|
+
* bricked every later permission that person minted. It names the grant now, so a revoke stops
|
|
593
|
+
* exactly what it was aimed at.
|
|
594
|
+
*/
|
|
595
|
+
if (revocationRequested(services.rig.rig, input.userId, grant.expiresAt)) {
|
|
596
|
+
return refuse('DELEGATION_NOT_ACTIVE', 'revocation was requested for this delegation on this server; nothing is submitted under it');
|
|
597
|
+
}
|
|
437
598
|
const expiresAtMs = Date.parse(grant.expiresAt);
|
|
438
599
|
const validUntilMs = Date.parse(input.envelope.valid_until);
|
|
439
600
|
if (!Number.isFinite(expiresAtMs) ||
|
|
440
601
|
!Number.isFinite(validUntilMs) ||
|
|
441
602
|
expiresAtMs <= validUntilMs) {
|
|
442
|
-
return refuse('DELEGATION_NOT_ACTIVE', `the delegation expires at ${grant.expiresAt}, not after the envelope's valid_until ${input.envelope.valid_until};
|
|
603
|
+
return refuse('DELEGATION_NOT_ACTIVE', `the delegation expires at ${grant.expiresAt}, not after the envelope's valid_until ${input.envelope.valid_until}; prepare a plan whose validity fits this existing grant if feasible, otherwise delegated execution is unavailable`);
|
|
443
604
|
}
|
|
444
605
|
/**
|
|
445
606
|
* 3b. THE EXPIRY BOUND, ENFORCED WHERE OTTO IS ACTUALLY IN THE PATH.
|
|
@@ -466,7 +627,7 @@ export async function submitUnderDelegation(runtime, rawInput, caller, _signal)
|
|
|
466
627
|
*/
|
|
467
628
|
const remainingMs = expiresAtMs - nowMs;
|
|
468
629
|
if (remainingMs > DELEGATION_MAX_REMAINING_MS) {
|
|
469
|
-
return refuse('DELEGATION_EXPIRY_EXCEEDS_BOUND', `the delegation still has ${Math.floor(remainingMs / 86_400_000)} days to run, past the ${MODEL_B_DELEGATION_EXPIRY_MAX_DAYS}-day maximum;
|
|
630
|
+
return refuse('DELEGATION_EXPIRY_EXCEEDS_BOUND', `the delegation still has ${Math.floor(remainingMs / 86_400_000)} days to run, past the ${MODEL_B_DELEGATION_EXPIRY_MAX_DAYS}-day maximum; nothing is submitted. Check status and use Stop to revoke this permission if desired`);
|
|
470
631
|
}
|
|
471
632
|
// 4. ACCOUNT: the caller's prepared address must be one of THIS end user's accounts as CDP reports them.
|
|
472
633
|
const record = await services.rig.readEndUser(input.userId);
|
|
@@ -500,33 +661,265 @@ export async function submitUnderDelegation(runtime, rawInput, caller, _signal)
|
|
|
500
661
|
throw refusal;
|
|
501
662
|
throw error;
|
|
502
663
|
}
|
|
503
|
-
|
|
504
|
-
|
|
664
|
+
/**
|
|
665
|
+
* 🔴 v2 — TWO SHAPES THROUGH ONE GATE. A delegated plan spends either an allow-listed INPUT token (a swap,
|
|
666
|
+
* or a Yield Copilot DEPOSIT, which is a swap into vault shares) — capped in USD by the engine fence and
|
|
667
|
+
* by the person's own recorded cap — or a registry vault SHARE token (a Yield Copilot WITHDRAW: shares →
|
|
668
|
+
* USDC, back to the signing account). A withdraw returns the person's own assets to the person and never
|
|
669
|
+
* increases exposure, so the person's per-swap cap does not apply to it: the cap checks below run with a
|
|
670
|
+
* gross of ZERO (which still validates the recorded cap's sanity). What bounds a withdraw is the engine's
|
|
671
|
+
* share cap (`assertWithdrawWithinModelBFence`) and the verifier's receiver pin, and it is admitted ONLY
|
|
672
|
+
* under fence v2 and ONLY for a permission minted under cap ruleset v2 (the person consented to withdraws).
|
|
673
|
+
*/
|
|
674
|
+
const inputTokens = services.fenceBound?.inputTokens ?? MODEL_B_INPUT_TOKENS_V1;
|
|
675
|
+
// A withdraw is recognised by its INPUT being a registry share token; anything else takes the swap path
|
|
676
|
+
// (and a token outside the allow-list is refused there, by the original message).
|
|
677
|
+
const isWithdraw = modelBWithdrawToken(verified.token) !== undefined;
|
|
678
|
+
let capGross;
|
|
679
|
+
let capToken;
|
|
680
|
+
if (isWithdraw) {
|
|
681
|
+
if (fence.version < 2) {
|
|
682
|
+
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`);
|
|
683
|
+
}
|
|
684
|
+
assertWithdrawWithinModelBFence({
|
|
685
|
+
token: verified.token,
|
|
686
|
+
spender: verified.spender,
|
|
687
|
+
gross: verified.gross,
|
|
688
|
+
outputToken: verified.outputToken,
|
|
689
|
+
});
|
|
690
|
+
capGross = 0n;
|
|
691
|
+
capToken = MODEL_B_INPUT_TOKENS_V1[0];
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
const bound = assertWithinModelBFence(verified, services.fenceBound?.perSwapCapUsd, inputTokens);
|
|
695
|
+
capGross = verified.gross;
|
|
696
|
+
capToken = bound.token;
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* 5b. THE SEND GATE (Founder word `delegate cap choice`; ROUND 4, built to the codex gate's own
|
|
700
|
+
* prescription after it NO-GO'd rounds 1–3).
|
|
701
|
+
*
|
|
702
|
+
* ══════════════════════════════════════════════════════════════════════════════════════════════
|
|
703
|
+
* 🔴 WHAT THIS REFUSES, AND WHY IT IS A CONFIRMED RECORD RATHER THAN A ROW
|
|
704
|
+
* ══════════════════════════════════════════════════════════════════════════════════════════════
|
|
705
|
+
* Round 3 read a durable row keyed by `(userId, grant.expiresAt)`. The gate's two CRITICALs:
|
|
706
|
+
*
|
|
707
|
+
* "The durable row still authenticates an expiry, not a grant … it does not require a successful
|
|
708
|
+
* confirmation, the reservation's mintKey, or an active-grant pointer."
|
|
709
|
+
* "Confirmation is not an enforcement gate. Reserve writes the complete enforceable capability;
|
|
710
|
+
* confirm writes no state at all."
|
|
711
|
+
*
|
|
712
|
+
* So a reservation was a bearer capability: anyone who could ask CDP for a grant at that instant —
|
|
713
|
+
* the agent CLI, an SDK client, a second tab — inherited the cap without confirming.
|
|
714
|
+
*
|
|
715
|
+
* The gate now asks for the CONFIRMED-ACTIVE record and nothing else. A reserved, superseded,
|
|
716
|
+
* revoked or expired record is not returned by `readActive` at all, so:
|
|
717
|
+
*
|
|
718
|
+
* · a grant minted at a reserved instant WITHOUT confirming is refused `DELEGATION_CAP_UNCONFIRMED`;
|
|
719
|
+
* · a grant that is not the one the confirmed record names — an old expiry reused, a permission
|
|
720
|
+
* minted elsewhere — is refused `DELEGATION_CAP_MISMATCH`;
|
|
721
|
+
* · a record from a NEWER cap ruleset than this worker implements is refused
|
|
722
|
+
* `DELEGATION_CAP_RULESET_STALE`, which is what stops a rolling deploy's older binary executing
|
|
723
|
+
* a new permission at the shared ceiling.
|
|
724
|
+
*
|
|
725
|
+
* None of them fall back to the engine's $5,000: "missing means the ceiling" would turn every one of
|
|
726
|
+
* these into a silent widening of somebody's permission.
|
|
727
|
+
*
|
|
728
|
+
* 🔴 AND IT IS RE-DERIVED FROM CURRENT DURABLE STATE ON EVERY READ, never from a boot-time map, so a
|
|
729
|
+
* process that missed another's confirmation still sees it — and a store that cannot be read THROWS
|
|
730
|
+
* rather than answering "nothing is confirmed", because those two have opposite consequences.
|
|
731
|
+
*/
|
|
732
|
+
const readActiveCap = async () => {
|
|
733
|
+
if (!services.capStore.available) {
|
|
734
|
+
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'})`);
|
|
735
|
+
}
|
|
736
|
+
let active;
|
|
737
|
+
try {
|
|
738
|
+
active = await services.capStore.readActive(input.userId, runtime.nowMs());
|
|
739
|
+
}
|
|
740
|
+
catch {
|
|
741
|
+
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');
|
|
742
|
+
}
|
|
743
|
+
if (!active) {
|
|
744
|
+
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. Check status and use Stop if a grant remains active');
|
|
745
|
+
}
|
|
746
|
+
if (active.requiredVersion > MODEL_B_CAP_RULESET_VERSION) {
|
|
747
|
+
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`);
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* 🔴 THE CONFIRMED RECORD MUST NAME THE GRANT THAT IS ACTUALLY LIVE. Without this the record would
|
|
751
|
+
* be a standing authorisation for whatever grant the user happens to hold, which is the round-3
|
|
752
|
+
* defect one layer up.
|
|
753
|
+
*/
|
|
754
|
+
if (Date.parse(active.expiresAt) !== expiresAtMs) {
|
|
755
|
+
return refuse('DELEGATION_CAP_MISMATCH', 'the delegation that reads back is not the one this permission was confirmed for; nothing is submitted. Check status and use Stop if you want to revoke the current grant');
|
|
756
|
+
}
|
|
757
|
+
return {
|
|
758
|
+
capUsd: active.perSwapCapUsd,
|
|
759
|
+
expiresAt: active.expiresAt,
|
|
760
|
+
requiredVersion: active.requiredVersion,
|
|
761
|
+
};
|
|
762
|
+
};
|
|
763
|
+
const active = await readActiveCap();
|
|
764
|
+
const userCapUsd = active.capUsd;
|
|
765
|
+
if (isWithdraw && active.requiredVersion < 2) {
|
|
766
|
+
return refuse('DELEGATION_WITHDRAW_UNCONSENTED', `this permission was minted under cap ruleset v${active.requiredVersion}, before vault withdraws existed; nothing is submitted. Vault withdrawal is unavailable under this grant; use an independently qualifying user-signed exit or seek support. Stop only revokes future delegated use`);
|
|
767
|
+
}
|
|
768
|
+
assertWithinUserPerSwapCap(capGross, userCapUsd, capToken);
|
|
769
|
+
/**
|
|
770
|
+
* 🔴 AND IT IS RE-CHECKED BEFORE EVERY STEP, AND AGAIN AFTER THE TRANSACTION IS BUILT.
|
|
771
|
+
*
|
|
772
|
+
* The plan is three transactions. `beforeEachStep` re-reads the grant and the confirmed record
|
|
773
|
+
* between them, which catches a revoke-and-re-mint from another tab. The gate found the remaining
|
|
774
|
+
* window: "the final asynchronous grant/cap check still happens before transaction construction …
|
|
775
|
+
* only a synchronous abort check precedes raw submission. A revoke/re-mint to a narrower cap during
|
|
776
|
+
* transaction building therefore escapes the last authoritative check."
|
|
777
|
+
*
|
|
778
|
+
* So the async re-read stores what it saw, and a SYNCHRONOUS assertion over that value runs after
|
|
779
|
+
* the nonce/fee/gas round trips and immediately before the hand-off. It cannot await — awaiting is
|
|
780
|
+
* what opened the window — so what it closes is the interval between the last read and the send,
|
|
781
|
+
* using the freshest value that exists. The irreducible residual is the network round trip itself,
|
|
782
|
+
* which no vendor CAS is offered to close.
|
|
783
|
+
*/
|
|
784
|
+
let lastSeenCapUsd = userCapUsd;
|
|
785
|
+
let lastSeenRequiredVersion = active.requiredVersion;
|
|
786
|
+
let lastSeenGrantOk = true;
|
|
787
|
+
/** Re-read the grant and the confirmed record; record what was seen. Used by both hooks below. */
|
|
788
|
+
const refreshSeen = async () => {
|
|
789
|
+
const current = await services.rig.rig.readDelegation(input.userId);
|
|
790
|
+
lastSeenGrantOk = current !== undefined && Date.parse(current.expiresAt) === expiresAtMs;
|
|
791
|
+
if (!lastSeenGrantOk)
|
|
792
|
+
return;
|
|
793
|
+
const seen = await readActiveCap();
|
|
794
|
+
lastSeenCapUsd = seen.capUsd;
|
|
795
|
+
lastSeenRequiredVersion = seen.requiredVersion;
|
|
796
|
+
};
|
|
797
|
+
/** A withdraw's consent is re-asserted wherever the cap is (codex gate r1: true at all three points). */
|
|
798
|
+
const assertWithdrawConsent = () => {
|
|
799
|
+
if (isWithdraw && lastSeenRequiredVersion < 2) {
|
|
800
|
+
return refuse('DELEGATION_WITHDRAW_UNCONSENTED', 'the confirmed permission changed to one minted before vault withdraws existed; the remaining steps are not submitted');
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
/** Between steps: the plan must still be admissible before the next one is even built. */
|
|
804
|
+
const beforeEachStep = async () => {
|
|
805
|
+
await refreshSeen();
|
|
806
|
+
if (!lastSeenGrantOk) {
|
|
807
|
+
return refuse('DELEGATION_NOT_ACTIVE', 'the delegation changed while the plan was in flight; the remaining steps are not submitted');
|
|
808
|
+
}
|
|
809
|
+
assertWithdrawConsent();
|
|
810
|
+
assertWithinUserPerSwapCap(capGross, lastSeenCapUsd, capToken);
|
|
811
|
+
};
|
|
812
|
+
/**
|
|
813
|
+
* 🔴 AND AGAIN AFTER THE TRANSACTION IS BUILT — the window the gate named. The nonce, fee and gas
|
|
814
|
+
* round trips take real time, and a revoke or a narrower re-mint landing inside them was invisible
|
|
815
|
+
* to the check between steps. This one runs after construction; the synchronous assertion below then
|
|
816
|
+
* acts on what it found, with nothing awaited between it and the hand-off.
|
|
817
|
+
*/
|
|
818
|
+
const afterBuild = refreshSeen;
|
|
819
|
+
const beforeSubmit = () => {
|
|
820
|
+
if (!lastSeenGrantOk) {
|
|
821
|
+
return refuse('DELEGATION_NOT_ACTIVE', 'the delegation changed while this transaction was being built; it is not submitted');
|
|
822
|
+
}
|
|
823
|
+
assertWithdrawConsent();
|
|
824
|
+
assertWithinUserPerSwapCap(capGross, lastSeenCapUsd, capToken);
|
|
825
|
+
};
|
|
826
|
+
// 6. CLAIM DURABLY, THEN SEND. The partial UNIQUE index is the cross-process per-user lock.
|
|
827
|
+
let begun;
|
|
828
|
+
try {
|
|
829
|
+
if (!services.workerGuards?.available)
|
|
830
|
+
throw new Error('worker guard store unavailable');
|
|
831
|
+
// Only a genuinely new artifact may create its permanent guard. If a durable row already exists,
|
|
832
|
+
// missing/corrupt guard metadata is a hard recovery refusal; recreating it could manufacture
|
|
833
|
+
// ownership while an old worker still holds a deleted or replaced inode.
|
|
834
|
+
const durableBeforeGuard = await services.operationStore.readArtifact(binding.artifactId);
|
|
835
|
+
if (durableBeforeGuard &&
|
|
836
|
+
(durableBeforeGuard.state === 'confirmed' || durableBeforeGuard.state === 'failed'))
|
|
837
|
+
return durableReplayOrRefusal(durableBeforeGuard, binding);
|
|
838
|
+
durableWorkerGuard = await services.workerGuards.acquire('delegated', binding.artifactId, durableBeforeGuard === undefined);
|
|
839
|
+
begun = await services.operationStore.begin(binding, runtime.nowMs());
|
|
840
|
+
}
|
|
841
|
+
catch (error) {
|
|
842
|
+
if (error instanceof ExecutionWorkerBusyError) {
|
|
843
|
+
return refuse('DELEGATION_ARTIFACT_IN_FLIGHT', 'the durable worker for this artifact is still active; wait for its result');
|
|
844
|
+
}
|
|
845
|
+
if (error instanceof DelegationOperationJournalNotReadyError) {
|
|
846
|
+
return refuse('DELEGATION_OPERATION_JOURNAL_NOT_READY', 'delegated execution is paused until every pre-journal worker has drained and journal coverage is marked ready');
|
|
847
|
+
}
|
|
848
|
+
return refuse('DELEGATION_OPERATION_STORE_UNAVAILABLE', 'the delegated operation could not be claimed durably; nothing is submitted');
|
|
849
|
+
}
|
|
850
|
+
if (begun.outcome === 'mismatch') {
|
|
851
|
+
return refuse('DELEGATION_REPLAY_MISMATCH', 'this artifact is durably bound to a different request; nothing is submitted');
|
|
852
|
+
}
|
|
853
|
+
if (begun.outcome === 'busy') {
|
|
854
|
+
return refuse('DELEGATION_OPERATION_UNRESOLVED', `another delegated operation (${begun.record.artifactId}) for this end user is ${begun.record.state}; resolve it before starting a conflicting send`);
|
|
855
|
+
}
|
|
856
|
+
if (begun.outcome === 'conflict') {
|
|
857
|
+
return refuse('DELEGATION_OPERATION_UNRESOLVED', `this end user has an unresolved ${begun.kind} operation (${begun.claimId}); resolve it before starting a delegated send`);
|
|
858
|
+
}
|
|
859
|
+
if (begun.outcome === 'existing')
|
|
860
|
+
return durableReplayOrRefusal(begun.record, binding);
|
|
861
|
+
durableStarted = true;
|
|
862
|
+
// Seal every static success field while no request has reached CDP. A replacement process can then
|
|
863
|
+
// reconstruct the exact three-step result from canonical stored receipts without re-running policy,
|
|
864
|
+
// quote or verification logic against newer state. A pre-upgrade row has no context and stays open.
|
|
865
|
+
const recoveryContext = verifiedRecoveryContext(input, user, verified, { id: fence.id, rulesDigest: fence.rulesDigest, userCapUsd, name: fence.name }, grant.expiresAt);
|
|
866
|
+
await services.operationStore.setRecoveryContext(binding, recoveryContext, runtime.nowMs());
|
|
505
867
|
const signer = new CdpDelegatedSigner(services.rig.rig, user, {
|
|
506
868
|
sealedWait: services.sealedWait,
|
|
869
|
+
beforeEachStep,
|
|
870
|
+
afterBuild,
|
|
871
|
+
beforeSubmit,
|
|
872
|
+
// The grant this plan is bound to: the revocation marker is per grant, so the signer names one.
|
|
873
|
+
grantExpiresAt: grant.expiresAt,
|
|
874
|
+
submissionJournal: operationJournal(services.operationStore, binding, runtime.nowMs),
|
|
507
875
|
});
|
|
508
876
|
let sent;
|
|
509
877
|
try {
|
|
510
878
|
sent = await signer.sendPlan(verified);
|
|
879
|
+
allPlanStepsConfirmed = true;
|
|
511
880
|
}
|
|
512
881
|
catch (error) {
|
|
882
|
+
retainUnresolvedAfterHalt = error instanceof DelegatedPlanHaltedError;
|
|
513
883
|
// Something may have reached the chain: this artifact is never retried on this server.
|
|
514
884
|
const refusal = toDelegationRefusal(error) ??
|
|
515
885
|
new ExecutionRefusal('DELEGATED_PLAN_HALTED', 'delegated plan halted (upstream_failure); the artifact is not retried on this server — prepare again');
|
|
516
886
|
submission.state = { kind: 'halted', refusal };
|
|
887
|
+
if (!retainUnresolvedAfterHalt) {
|
|
888
|
+
try {
|
|
889
|
+
await services.operationStore.finalizeFailure(binding, refusal.code, runtime.nowMs());
|
|
890
|
+
}
|
|
891
|
+
catch {
|
|
892
|
+
// A failed finalization leaves the partial-index guard unresolved. Never retry CDP or erase it.
|
|
893
|
+
}
|
|
894
|
+
}
|
|
517
895
|
throw refusal;
|
|
518
896
|
}
|
|
519
|
-
|
|
897
|
+
if (!(await liveDelegatedSuccessIsFinalized(services, binding))) {
|
|
898
|
+
return refuse('DELEGATION_OPERATION_UNRESOLVED', 'all three delegated transactions were included, but their exact receipts are still pending Base finality; the operation and unified claim remain open — use Check Base receipts to reconcile it later');
|
|
899
|
+
}
|
|
900
|
+
const result = verifiedResult(input, user, verified, sent, { id: fence.id, rulesDigest: fence.rulesDigest, userCapUsd, name: fence.name }, grant.expiresAt);
|
|
901
|
+
await services.operationStore.finalizeSuccess(binding, result, runtime.nowMs());
|
|
520
902
|
submission.state = { kind: 'sent', result };
|
|
521
903
|
return result;
|
|
522
904
|
}
|
|
523
905
|
catch (error) {
|
|
524
906
|
// A refusal BEFORE any send leaves no trace: the caller may fix the request and resubmit the same artifact.
|
|
525
|
-
if (submission.state.kind === 'in_flight')
|
|
907
|
+
if (submission.state.kind === 'in_flight') {
|
|
908
|
+
if (durableStarted && !allPlanStepsConfirmed && !retainUnresolvedAfterHalt) {
|
|
909
|
+
const code = isExecutionRefusal(error) ? error.code : 'DELEGATION_UPSTREAM_FAILED';
|
|
910
|
+
try {
|
|
911
|
+
await services.operationStore.finalizeFailure(binding, code, runtime.nowMs());
|
|
912
|
+
}
|
|
913
|
+
catch {
|
|
914
|
+
// The unresolved row is the safety outcome. Do not mask the original error.
|
|
915
|
+
}
|
|
916
|
+
}
|
|
526
917
|
records.delete(artifactId);
|
|
918
|
+
}
|
|
527
919
|
throw error;
|
|
528
920
|
}
|
|
529
921
|
finally {
|
|
922
|
+
durableWorkerGuard?.release();
|
|
530
923
|
release();
|
|
531
924
|
}
|
|
532
925
|
}
|
|
@@ -574,9 +967,26 @@ export async function openDelegationServicesFromEnv(env) {
|
|
|
574
967
|
if (chainId !== MODEL_B_CHAIN_ID) {
|
|
575
968
|
throw new Error(`BASE_RPC_URL reports chainId ${chainId}, expected ${MODEL_B_CHAIN_ID} (Base mainnet)`);
|
|
576
969
|
}
|
|
970
|
+
const operationRoot = resolveDelegationCapStorePath();
|
|
577
971
|
return Object.freeze({
|
|
578
972
|
rig: createDelegationRig(cdp, pub),
|
|
973
|
+
pub,
|
|
579
974
|
serverSecret: env.serverSecret,
|
|
975
|
+
// 🔴 An unconfigured store does NOT stop the boot — it opens UNAVAILABLE, and every delegated send
|
|
976
|
+
// then refuses `DELEGATION_CAP_STORE_UNAVAILABLE` by name. Refusing to boot would also take down the
|
|
977
|
+
// read-only tools (status, revoke, the public fence promise) and, in particular, the REVOKE that lets
|
|
978
|
+
// a person withdraw authority — the one control that must never be collateral damage.
|
|
979
|
+
capStore: await createDelegationCapStore(),
|
|
980
|
+
operationStore: await createDelegationOperationStore(),
|
|
981
|
+
exitStore: await createDelegationExitStore(),
|
|
982
|
+
workerGuards: operationRoot
|
|
983
|
+
? await openExecutionWorkerGuardStore(operationRoot)
|
|
984
|
+
: unavailableExecutionWorkerGuardStore('no durable operation guard directory is configured'),
|
|
985
|
+
// Default FALSE in code: an operator who never sets the variable gets the paused surface, never an
|
|
986
|
+
// unfenced first rollout. Read once at boot, so a mid-life environment change needs a redeploy —
|
|
987
|
+
// which is exactly what the rollout runbook does.
|
|
988
|
+
mintEnabled: resolveDelegationMintEnabled(),
|
|
989
|
+
tradeMintEnabled: false,
|
|
580
990
|
...(env.publicProjectId ? { publicProjectId: env.publicProjectId } : {}),
|
|
581
991
|
});
|
|
582
992
|
}
|
|
@@ -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 ["HYPERLIQUID_RETIRED", "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_OPERATION_STORE_UNAVAILABLE", "DELEGATION_OPERATION_JOURNAL_NOT_READY", "DELEGATION_OPERATION_UNRESOLVED", "DELEGATION_EXIT_UNAVAILABLE", "DELEGATION_EXIT_NOT_FOUND", "DELEGATION_EXIT_MISMATCH", "DELEGATION_EXIT_STALE_OWNER", "DELEGATION_EXIT_GRANT_ACTIVE", "DELEGATION_EXIT_UNRESOLVED", "DELEGATION_MINT_PAUSED", "DELEGATION_CAP_CANCELLED", "DELEGATED_PLAN_REFUSED", "DELEGATED_PLAN_HALTED", "DELEGATION_UPSTREAM_FAILED", "YIELD_MARKET_UNKNOWN", "YIELD_DEPOSIT_UNAVAILABLE", "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 {
|