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.
Files changed (239) hide show
  1. package/README.md +76 -18
  2. package/dist/account-userop-policy-definition.d.ts +14 -0
  3. package/dist/account-userop-policy-definition.js +12 -0
  4. package/dist/account-userop-policy-relation.d.ts +371 -0
  5. package/dist/account-userop-policy-relation.js +87 -0
  6. package/dist/adapter/cdp-signer.d.ts +76 -9
  7. package/dist/adapter/cdp-signer.js +274 -33
  8. package/dist/adapter/evm-call-failure.d.ts +29 -0
  9. package/dist/adapter/evm-call-failure.js +109 -0
  10. package/dist/adapter/refusal.d.ts +1 -1
  11. package/dist/adapter/refusal.js +13 -0
  12. package/dist/adapter/route-simulator.d.ts +91 -0
  13. package/dist/adapter/route-simulator.js +143 -0
  14. package/dist/adapter-cdp-index.d.ts +1 -1
  15. package/dist/executable-equities.d.ts +55 -0
  16. package/dist/executable-equities.js +116 -0
  17. package/dist/executable-yield-markets.d.ts +261 -0
  18. package/dist/executable-yield-markets.js +154 -0
  19. package/dist/execution-account-gas-quote.d.ts +141 -0
  20. package/dist/execution-account-gas-quote.js +443 -0
  21. package/dist/execution-account-gas-review.d.ts +123 -0
  22. package/dist/execution-account-gas-review.js +104 -0
  23. package/dist/execution-account-gas-runtime.d.ts +33 -0
  24. package/dist/execution-account-gas-runtime.js +131 -0
  25. package/dist/execution-account-gas-view.d.ts +936 -0
  26. package/dist/execution-account-gas-view.js +56 -0
  27. package/dist/execution-config.d.ts +3 -0
  28. package/dist/execution-config.js +4 -0
  29. package/dist/execution-delegated-definition.d.ts +7 -6
  30. package/dist/execution-delegated-definition.js +13 -2
  31. package/dist/execution-delegation-admin-definition.d.ts +127 -5
  32. package/dist/execution-delegation-admin-definition.js +155 -5
  33. package/dist/execution-delegation-admin.d.ts +48 -2
  34. package/dist/execution-delegation-admin.js +422 -12
  35. package/dist/execution-delegation-cap-store.d.ts +386 -0
  36. package/dist/execution-delegation-cap-store.js +1150 -0
  37. package/dist/execution-delegation-exit-definition.d.ts +309 -0
  38. package/dist/execution-delegation-exit-definition.js +204 -0
  39. package/dist/execution-delegation-exit-store.d.ts +114 -0
  40. package/dist/execution-delegation-exit-store.js +671 -0
  41. package/dist/execution-delegation-exit.d.ts +4 -0
  42. package/dist/execution-delegation-exit.js +614 -0
  43. package/dist/execution-delegation-finality.d.ts +22 -0
  44. package/dist/execution-delegation-finality.js +90 -0
  45. package/dist/execution-delegation-operation-store.d.ts +129 -0
  46. package/dist/execution-delegation-operation-store.js +1107 -0
  47. package/dist/execution-delegation-operations-definition.d.ts +282 -0
  48. package/dist/execution-delegation-operations-definition.js +117 -0
  49. package/dist/execution-delegation-operations.d.ts +6 -0
  50. package/dist/execution-delegation-operations.js +282 -0
  51. package/dist/execution-delegation-policy.d.ts +154 -8
  52. package/dist/execution-delegation-policy.js +273 -40
  53. package/dist/execution-delegation.d.ts +51 -0
  54. package/dist/execution-delegation.js +441 -31
  55. package/dist/execution-errors.d.ts +1 -1
  56. package/dist/execution-errors.js +122 -0
  57. package/dist/execution-index.d.ts +7 -2
  58. package/dist/execution-index.js +7 -2
  59. package/dist/execution-journal-schema.d.ts +96 -0
  60. package/dist/execution-journal-schema.js +368 -0
  61. package/dist/execution-journal-tables.d.ts +14 -0
  62. package/dist/execution-journal-tables.js +227 -0
  63. package/dist/execution-policy-compatibility.d.ts +34 -0
  64. package/dist/execution-policy-compatibility.js +76 -0
  65. package/dist/execution-registration.d.ts +54 -2
  66. package/dist/execution-registration.js +151 -5
  67. package/dist/execution-sponsored-account-profile.d.ts +32 -0
  68. package/dist/execution-sponsored-account-profile.js +59 -0
  69. package/dist/execution-sponsored-account.d.ts +1519 -0
  70. package/dist/execution-sponsored-account.js +222 -0
  71. package/dist/execution-sponsored-definition.d.ts +1713 -0
  72. package/dist/execution-sponsored-definition.js +185 -0
  73. package/dist/execution-sponsored-gas-policy.d.ts +27 -0
  74. package/dist/execution-sponsored-gas-policy.js +110 -0
  75. package/dist/execution-sponsored-hosted.d.ts +3 -0
  76. package/dist/execution-sponsored-hosted.js +71 -0
  77. package/dist/execution-sponsored-index.d.ts +1322 -0
  78. package/dist/execution-sponsored-index.js +102 -0
  79. package/dist/execution-sponsored-mcp-definition.d.ts +1662 -0
  80. package/dist/execution-sponsored-mcp-definition.js +94 -0
  81. package/dist/execution-sponsored-receipt.d.ts +47 -0
  82. package/dist/execution-sponsored-receipt.js +74 -0
  83. package/dist/execution-sponsored-registration.d.ts +14 -0
  84. package/dist/execution-sponsored-registration.js +77 -0
  85. package/dist/execution-sponsored-runtime.d.ts +84 -0
  86. package/dist/execution-sponsored-runtime.js +149 -0
  87. package/dist/execution-sponsored-settlement.d.ts +15 -0
  88. package/dist/execution-sponsored-settlement.js +88 -0
  89. package/dist/execution-sponsored-store.d.ts +67 -0
  90. package/dist/execution-sponsored-store.js +517 -0
  91. package/dist/execution-sponsored-transport.d.ts +90 -0
  92. package/dist/execution-sponsored-transport.js +180 -0
  93. package/dist/execution-tool-definitions.d.ts +21 -21
  94. package/dist/execution-tool-definitions.js +0 -7
  95. package/dist/execution-tools.d.ts +53 -1
  96. package/dist/execution-tools.js +171 -13
  97. package/dist/execution-worker-guard.d.ts +28 -0
  98. package/dist/execution-worker-guard.js +124 -0
  99. package/dist/hybrid-yield-index.d.ts +10 -0
  100. package/dist/hybrid-yield-index.js +11 -0
  101. package/dist/hybrid-yield-markets.d.ts +96 -0
  102. package/dist/hybrid-yield-markets.js +124 -0
  103. package/dist/hybrid-yield.d.ts +411 -0
  104. package/dist/hybrid-yield.js +155 -0
  105. package/dist/hyperliquid-order-builder.js +4 -0
  106. package/dist/lifi-execution-client.d.ts +6 -0
  107. package/dist/lifi-execution-client.js +3 -0
  108. package/dist/lp-userop-account.d.ts +461 -0
  109. package/dist/lp-userop-account.js +417 -0
  110. package/dist/lp-userop-authority.d.ts +49 -0
  111. package/dist/lp-userop-authority.js +211 -0
  112. package/dist/lp-userop-definition.d.ts +233 -0
  113. package/dist/lp-userop-definition.js +85 -0
  114. package/dist/lp-userop-hosted.d.ts +3 -0
  115. package/dist/lp-userop-hosted.js +73 -0
  116. package/dist/lp-userop-index.d.ts +1569 -0
  117. package/dist/lp-userop-index.js +112 -0
  118. package/dist/lp-userop-mcp-definition.d.ts +1220 -0
  119. package/dist/lp-userop-mcp-definition.js +46 -0
  120. package/dist/lp-userop-operation.d.ts +1230 -0
  121. package/dist/lp-userop-operation.js +140 -0
  122. package/dist/lp-userop-registration.d.ts +14 -0
  123. package/dist/lp-userop-registration.js +70 -0
  124. package/dist/lp-userop-settlement.d.ts +89 -0
  125. package/dist/lp-userop-settlement.js +184 -0
  126. package/dist/lp-userop.d.ts +643 -0
  127. package/dist/lp-userop.js +343 -0
  128. package/dist/native-yield-account-definition.d.ts +1192 -0
  129. package/dist/native-yield-account-definition.js +232 -0
  130. package/dist/native-yield-account-gas.d.ts +58 -0
  131. package/dist/native-yield-account-gas.js +228 -0
  132. package/dist/native-yield-account-index.d.ts +23 -0
  133. package/dist/native-yield-account-index.js +28 -0
  134. package/dist/native-yield-account-policy.d.ts +168 -0
  135. package/dist/native-yield-account-policy.js +525 -0
  136. package/dist/native-yield-account-rpc.d.ts +8 -0
  137. package/dist/native-yield-account-rpc.js +128 -0
  138. package/dist/native-yield-account-runtime.d.ts +71 -0
  139. package/dist/native-yield-account-runtime.js +125 -0
  140. package/dist/native-yield-account-store.d.ts +40 -0
  141. package/dist/native-yield-account-store.js +307 -0
  142. package/dist/native-yield-account.d.ts +662 -0
  143. package/dist/native-yield-account.js +371 -0
  144. package/dist/native-yield-consent-store.d.ts +20 -0
  145. package/dist/native-yield-consent-store.js +317 -0
  146. package/dist/native-yield-definition.d.ts +288 -0
  147. package/dist/native-yield-definition.js +105 -0
  148. package/dist/native-yield-reader.d.ts +4 -0
  149. package/dist/native-yield-reader.js +137 -0
  150. package/dist/native-yield-settlement.d.ts +48 -0
  151. package/dist/native-yield-settlement.js +167 -0
  152. package/dist/native-yield-simulation.d.ts +18 -0
  153. package/dist/native-yield-simulation.js +45 -0
  154. package/dist/native-yield-userop-account.d.ts +675 -0
  155. package/dist/native-yield-userop-account.js +357 -0
  156. package/dist/native-yield-userop-authority.d.ts +214 -0
  157. package/dist/native-yield-userop-authority.js +296 -0
  158. package/dist/native-yield-userop-definition.d.ts +331 -0
  159. package/dist/native-yield-userop-definition.js +105 -0
  160. package/dist/native-yield-userop-hosted.d.ts +3 -0
  161. package/dist/native-yield-userop-hosted.js +71 -0
  162. package/dist/native-yield-userop-index.d.ts +1522 -0
  163. package/dist/native-yield-userop-index.js +142 -0
  164. package/dist/native-yield-userop-mcp-definition.d.ts +1356 -0
  165. package/dist/native-yield-userop-mcp-definition.js +50 -0
  166. package/dist/native-yield-userop-operation.d.ts +845 -0
  167. package/dist/native-yield-userop-operation.js +127 -0
  168. package/dist/native-yield-userop-policy.d.ts +23 -0
  169. package/dist/native-yield-userop-policy.js +25 -0
  170. package/dist/native-yield-userop-registration.d.ts +17 -0
  171. package/dist/native-yield-userop-registration.js +76 -0
  172. package/dist/native-yield-userop-settlement.d.ts +8 -0
  173. package/dist/native-yield-userop-settlement.js +79 -0
  174. package/dist/native-yield-userop-simulation.d.ts +18 -0
  175. package/dist/native-yield-userop-simulation.js +34 -0
  176. package/dist/native-yield-userop.d.ts +35 -0
  177. package/dist/native-yield-userop.js +156 -0
  178. package/dist/native-yield-verify.d.ts +30 -0
  179. package/dist/native-yield-verify.js +141 -0
  180. package/dist/native-yield.d.ts +17 -0
  181. package/dist/native-yield.js +119 -0
  182. package/dist/stdio-main.js +0 -0
  183. package/dist/stock-buy-definition.d.ts +338 -0
  184. package/dist/stock-buy-definition.js +127 -0
  185. package/dist/stock-buy-oracle.d.ts +8 -0
  186. package/dist/stock-buy-oracle.js +22 -0
  187. package/dist/stock-buy.d.ts +17 -0
  188. package/dist/stock-buy.js +164 -0
  189. package/dist/tool-definitions.d.ts +3 -1
  190. package/dist/tool-definitions.js +55 -2
  191. package/dist/vendor/lp-v3/lp-v3-atomic-contract.d.ts +445 -0
  192. package/dist/vendor/lp-v3/lp-v3-atomic-contract.js +90 -0
  193. package/dist/vendor/lp-v3/lp-v3-atomic-evidence.d.ts +14 -0
  194. package/dist/vendor/lp-v3/lp-v3-atomic-evidence.js +82 -0
  195. package/dist/vendor/lp-v3/lp-v3-atomic-index.d.ts +80 -0
  196. package/dist/vendor/lp-v3/lp-v3-atomic-index.js +19 -0
  197. package/dist/vendor/lp-v3/lp-v3-atomic-plan.d.ts +43 -0
  198. package/dist/vendor/lp-v3/lp-v3-atomic-plan.js +112 -0
  199. package/dist/vendor/lp-v3/lp-v3-atomic-read-contract.d.ts +312 -0
  200. package/dist/vendor/lp-v3/lp-v3-atomic-read-contract.js +41 -0
  201. package/dist/vendor/lp-v3/lp-v3-atomic-read-index.d.ts +3 -0
  202. package/dist/vendor/lp-v3/lp-v3-atomic-read-index.js +3 -0
  203. package/dist/vendor/lp-v3/lp-v3-atomic-read-rpc.d.ts +49 -0
  204. package/dist/vendor/lp-v3/lp-v3-atomic-read-rpc.js +88 -0
  205. package/dist/vendor/lp-v3/lp-v3-atomic-read.d.ts +17 -0
  206. package/dist/vendor/lp-v3/lp-v3-atomic-read.js +232 -0
  207. package/dist/vendor/lp-v3/lp-v3-atomic-settlement.d.ts +105 -0
  208. package/dist/vendor/lp-v3/lp-v3-atomic-settlement.js +162 -0
  209. package/dist/vendor/lp-v3/lp-v3-collect-protocol-contract.d.ts +606 -0
  210. package/dist/vendor/lp-v3/lp-v3-collect-protocol-contract.js +59 -0
  211. package/dist/vendor/lp-v3/lp-v3-collect-protocol-index.d.ts +168 -0
  212. package/dist/vendor/lp-v3/lp-v3-collect-protocol-index.js +23 -0
  213. package/dist/vendor/lp-v3/lp-v3-collect-protocol-observation.d.ts +32 -0
  214. package/dist/vendor/lp-v3/lp-v3-collect-protocol-observation.js +228 -0
  215. package/dist/vendor/lp-v3/lp-v3-collect-protocol-plan.d.ts +11 -0
  216. package/dist/vendor/lp-v3/lp-v3-collect-protocol-plan.js +91 -0
  217. package/dist/vendor/lp-v3/lp-v3-collect-protocol-reconcile.d.ts +9 -0
  218. package/dist/vendor/lp-v3/lp-v3-collect-protocol-reconcile.js +124 -0
  219. package/dist/vendor/lp-v3/lp-v3-contract.d.ts +538 -0
  220. package/dist/vendor/lp-v3/lp-v3-contract.js +137 -0
  221. package/dist/vendor/lp-v3/lp-v3-evidence.d.ts +25 -0
  222. package/dist/vendor/lp-v3/lp-v3-evidence.js +168 -0
  223. package/dist/vendor/lp-v3/lp-v3-plan.d.ts +22 -0
  224. package/dist/vendor/lp-v3/lp-v3-plan.js +135 -0
  225. package/dist/vendor/lp-v3/types.d.ts +131 -0
  226. package/dist/vendor/lp-v3/types.js +6 -0
  227. package/dist/vendor/lp-v3/v4-math.d.ts +18 -0
  228. package/dist/vendor/lp-v3/v4-math.js +90 -0
  229. package/dist/yield-deposit-definition.d.ts +374 -0
  230. package/dist/yield-deposit-definition.js +123 -0
  231. package/dist/yield-deposit.d.ts +16 -0
  232. package/dist/yield-deposit.js +149 -0
  233. package/dist/yield-route-semantics.d.ts +27 -0
  234. package/dist/yield-route-semantics.js +71 -0
  235. package/dist/yield-withdraw-definition.d.ts +357 -0
  236. package/dist/yield-withdraw-definition.js +90 -0
  237. package/dist/yield-withdraw.d.ts +11 -0
  238. package/dist/yield-withdraw.js +122 -0
  239. 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 { assertWithinModelBFence, MODEL_B_CHAIN_ID, MODEL_B_DELEGATION_EXPIRY_MAX_DAYS, MODEL_B_PER_SWAP_CAP_USD, MODEL_B_POLICY_NAME_V1, readBackModelBFence, } from './execution-delegation-policy.js';
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 verifiedResult(input, user, verified, sent, fence, expiresAt) {
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
- steps: sent.map((step) => Object.freeze({
352
- index: step.index,
353
- kind: step.kind,
354
- to: step.to,
355
- transaction_hash: step.transaction_hash,
356
- status: step.status,
357
- block_number: step.block_number,
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: MODEL_B_POLICY_NAME_V1,
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
- checks: verified.checks.map((check) => check.id),
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: the
391
- // idempotency record below is process-local, and a process that did not prepare the artifact cannot know
392
- // whether another one already sent it (a rolling replacement runs two at once). Refused before any read;
393
- // the caller prepares again. The commitment is RECOMPUTED from the presented envelope (the field it
394
- // carries is never trusted), so a tampered envelope under a known id is refused here, never replayed.
395
- // Checked BEFORE the record on purpose: a prepared-here entry is never evicted and expires exactly when
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 ever requested here.
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}; renew the delegation or prepare again`);
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; revoke it and mint a new one`);
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
- assertWithinModelBFence(verified, services.fenceBound?.perSwapCapUsd, services.fenceBound?.inputTokens);
504
- // 6. SEND.
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
- const result = verifiedResult(input, user, verified, sent, fence, grant.expiresAt);
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 {