otto-intel-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +102 -0
  3. package/dist/adapter/cdp-signer.d.ts +133 -0
  4. package/dist/adapter/cdp-signer.js +356 -0
  5. package/dist/adapter/cdp-signer.js.map +1 -0
  6. package/dist/adapter/chain.d.ts +36 -0
  7. package/dist/adapter/chain.js +65 -0
  8. package/dist/adapter/chain.js.map +1 -0
  9. package/dist/adapter/erc20.d.ts +39 -0
  10. package/dist/adapter/erc20.js +17 -0
  11. package/dist/adapter/erc20.js.map +1 -0
  12. package/dist/adapter/lifi-decode.d.ts +52 -0
  13. package/dist/adapter/lifi-decode.js +149 -0
  14. package/dist/adapter/lifi-decode.js.map +1 -0
  15. package/dist/adapter/refusal.d.ts +21 -0
  16. package/dist/adapter/refusal.js +55 -0
  17. package/dist/adapter/refusal.js.map +1 -0
  18. package/dist/adapter/sent-step.d.ts +15 -0
  19. package/dist/adapter/sent-step.js +6 -0
  20. package/dist/adapter/sent-step.js.map +1 -0
  21. package/dist/adapter/verify.d.ts +149 -0
  22. package/dist/adapter/verify.js +432 -0
  23. package/dist/adapter/verify.js.map +1 -0
  24. package/dist/adapter-cdp-index.d.ts +6 -0
  25. package/dist/adapter-cdp-index.js +7 -0
  26. package/dist/adapter-index.d.ts +15 -0
  27. package/dist/adapter-index.js +15 -0
  28. package/dist/artifact-id.d.ts +16 -0
  29. package/dist/artifact-id.js +60 -0
  30. package/dist/boot-redaction.d.ts +16 -0
  31. package/dist/boot-redaction.js +54 -0
  32. package/dist/data-source.d.ts +7 -0
  33. package/dist/data-source.js +2 -0
  34. package/dist/errors.d.ts +70 -0
  35. package/dist/errors.js +221 -0
  36. package/dist/execution-config.d.ts +232 -0
  37. package/dist/execution-config.js +443 -0
  38. package/dist/execution-delegated-definition.d.ts +165 -0
  39. package/dist/execution-delegated-definition.js +116 -0
  40. package/dist/execution-delegation-admin-definition.d.ts +208 -0
  41. package/dist/execution-delegation-admin-definition.js +170 -0
  42. package/dist/execution-delegation-admin.d.ts +74 -0
  43. package/dist/execution-delegation-admin.js +290 -0
  44. package/dist/execution-delegation-policy.d.ts +257 -0
  45. package/dist/execution-delegation-policy.js +279 -0
  46. package/dist/execution-delegation.d.ts +190 -0
  47. package/dist/execution-delegation.js +545 -0
  48. package/dist/execution-errors.d.ts +9 -0
  49. package/dist/execution-errors.js +128 -0
  50. package/dist/execution-index.d.ts +17 -0
  51. package/dist/execution-index.js +16 -0
  52. package/dist/execution-intent.d.ts +14 -0
  53. package/dist/execution-intent.js +36 -0
  54. package/dist/execution-registration.d.ts +113 -0
  55. package/dist/execution-registration.js +161 -0
  56. package/dist/execution-tool-definitions.d.ts +1103 -0
  57. package/dist/execution-tool-definitions.js +1051 -0
  58. package/dist/execution-tools.d.ts +85 -0
  59. package/dist/execution-tools.js +690 -0
  60. package/dist/execution-types.d.ts +274 -0
  61. package/dist/execution-types.js +157 -0
  62. package/dist/hyperliquid-info-client.d.ts +31 -0
  63. package/dist/hyperliquid-info-client.js +121 -0
  64. package/dist/hyperliquid-order-assertions.d.ts +89 -0
  65. package/dist/hyperliquid-order-assertions.js +325 -0
  66. package/dist/hyperliquid-order-builder.d.ts +47 -0
  67. package/dist/hyperliquid-order-builder.js +55 -0
  68. package/dist/lifi-bridge-assertions.d.ts +49 -0
  69. package/dist/lifi-bridge-assertions.js +303 -0
  70. package/dist/lifi-execution-client.d.ts +74 -0
  71. package/dist/lifi-execution-client.js +165 -0
  72. package/dist/lifi-fee-assertions.d.ts +32 -0
  73. package/dist/lifi-fee-assertions.js +103 -0
  74. package/dist/lifi-swap-assertions.d.ts +32 -0
  75. package/dist/lifi-swap-assertions.js +214 -0
  76. package/dist/local-config.d.ts +9 -0
  77. package/dist/local-config.js +45 -0
  78. package/dist/mcp-server.d.ts +4 -0
  79. package/dist/mcp-server.js +44 -0
  80. package/dist/polymarket-clob-client.d.ts +73 -0
  81. package/dist/polymarket-clob-client.js +186 -0
  82. package/dist/polymarket-order-assertions.d.ts +60 -0
  83. package/dist/polymarket-order-assertions.js +273 -0
  84. package/dist/polymarket-order-builder.d.ts +43 -0
  85. package/dist/polymarket-order-builder.js +74 -0
  86. package/dist/prepared-artifacts.d.ts +45 -0
  87. package/dist/prepared-artifacts.js +92 -0
  88. package/dist/stdio-main.d.ts +2 -0
  89. package/dist/stdio-main.js +20 -0
  90. package/dist/tool-definitions.d.ts +20 -0
  91. package/dist/tool-definitions.js +162 -0
  92. package/dist/x402-read-source.d.ts +11 -0
  93. package/dist/x402-read-source.js +77 -0
  94. package/package.json +162 -0
@@ -0,0 +1,290 @@
1
+ /**
2
+ * execution-delegation-admin.ts — the three delegation ADMIN tools on the hosted MCP (PR-1b, seat ruling D10 = (a)).
3
+ *
4
+ * otto_delegation_fence — the ONLY writer of the Model-B project policy: ensure-by-digest + read-back.
5
+ * otto_delegation_status — one user's grant/fence read-back, bound to the user a CDP access token resolves to.
6
+ * otto_delegation_revoke — developer-side revoke with read-back, fail-closed under the rig lock.
7
+ *
8
+ * Every tool: the request header FIRST (constant-time, before parsing or any CDP read — the same seam as
9
+ * `otto_submit_under_delegation`), then the strict schema, then the reads. Nothing here logs a token, the
10
+ * header, or a credential; refusals are machine-readable codes.
11
+ *
12
+ * FENCE RULES (gate r1 of this PR sharpened them; each is mutation-proved in the tests):
13
+ * CARDINALITY — Coinbase allows exactly one project-scope policy. A read-back with MORE than one is an
14
+ * anomalous snapshot and refuses (`DELEGATION_FENCE_AMBIGUOUS`) before any other judgement — nothing is
15
+ * written, nothing is blessed as present on the strength of "one of them matches".
16
+ * LINEAGE — a project policy is OURS only when its description matches the ANCHORED pattern
17
+ * `^OTTO model b swap fence v<N>$` (`modelBLineageVersion`). Ours with N == this binary's version and the
18
+ * v-current rules → `unchanged`. Ours with N ≤ this version and stale rules → updated IN PLACE. Ours with
19
+ * N > this version → refused (`DELEGATION_FENCE_NEWER`): a running older binary never downgrades a newer
20
+ * fence (rollback is a governed manual act). Anything else — the proof kit's `OTTO PROBE …`, a hand-installed
21
+ * policy, no description, the prefix in the middle, a suffix — is FOREIGN: refused by name
22
+ * (`DELEGATION_FENCE_FOREIGN`, naming the description + rules-digest prefix); never overwritten, never deleted.
23
+ * WRITE ONCE — a create that Coinbase refuses as a duplicate (`already_exists` / 409: someone created a policy
24
+ * between the snapshot and the write) is never retried and never turned into an update from this path: the
25
+ * tool re-reads once and judges what is there.
26
+ * BEFORE AN UPDATE — the policy is re-read BY ID immediately before the write and must still be exactly the
27
+ * snapshot (same description, same rules digest); any change → refused, nothing written. `@coinbase/cdp-sdk`
28
+ * 1.54.0 offers no conditional update (`UpdatePolicyOptions` = { id, policy, idempotencyKey } — no revision,
29
+ * no ETag), so the window between that re-read and the write cannot be closed by code. It is closed by the
30
+ * OPERATIONAL PREMISE, stated here, in the tool description and in the README: AT MOST ONE ENSURE WRITES
31
+ * PROJECT-WIDE AT A TIME, ACROSS VERSIONS — the ensure is an OPS action run by one operator, never during a
32
+ * rolling deploy (two versions of this tool running at once are two writers, and the older one would win a
33
+ * race and downgrade); the dApp never invokes it (PR-2's mint gate reads `fence.present` from
34
+ * `otto_delegation_status`); nobody edits the policy in the CDP portal while an ensure runs. Under that
35
+ * premise the re-read is exact; without it, "never overwrite foreign" and "never downgrade" narrow to
36
+ * "…what was foreign / newer one round-trip ago".
37
+ * READ-BACK BOUND TO THE WRITE — after a write, the proof is a read-back of exactly one project policy whose
38
+ * id IS the id that was written (the created id, or the updated id) and which reads back as v-current. A
39
+ * matching record under another id (a stale cache, a ghost) is not proof; a bounded poll covers the engine's
40
+ * asynchronous propagation, then the tool fails closed (`DELEGATION_FENCE_ABSENT`).
41
+ *
42
+ * IDENTITY. `status` and the user path of `revoke` resolve the userId from `cdp.endUser.validateAccessToken`;
43
+ * a client-sent id is refused by the strict schema. The support path of `revoke` takes `{ userId,
44
+ * support_reason }` from an operator holding the server secret; the reason is written to the structured log
45
+ * through the boot redactor (URLs and configured secret values never land in the log), the token never is.
46
+ */
47
+ import { getAddress } from 'viem';
48
+ import { revocationRequested } from './adapter/cdp-signer.js';
49
+ import { redactBootMessage } from './boot-redaction.js';
50
+ import { DELEGATION_FENCE_INPUT_SCHEMA, DELEGATION_REVOKE_INPUT_SCHEMA, DELEGATION_STATUS_INPUT_SCHEMA, } from './execution-delegation-admin-definition.js';
51
+ import { assertDelegationCaller, } from './execution-delegation.js';
52
+ import { MODEL_B_PER_SWAP_CAP_USD, MODEL_B_POLICY_LINEAGE_PREFIX, MODEL_B_POLICY_NAME_V1, MODEL_B_POLICY_VERSION, modelBLineageVersion, modelBProjectPolicyBody, policyRulesDigest, readBackModelBFence, } from './execution-delegation-policy.js';
53
+ import { refuse } from './execution-errors.js';
54
+ const DEFAULT_FENCE_WAIT = { pollMs: 500, maxPolls: 20 };
55
+ function servicesOf(runtime) {
56
+ const services = runtime.delegation;
57
+ if (!services)
58
+ return refuse('DELEGATION_UNAVAILABLE', 'delegated send is not configured on this server');
59
+ return services;
60
+ }
61
+ function parseWith(schema, rawInput) {
62
+ const parsed = schema.safeParse(rawInput);
63
+ if (!parsed.success) {
64
+ const issue = parsed.error.issues[0];
65
+ return refuse('DELEGATION_INPUT_INVALID', `input does not satisfy the schema: ${issue ? `${issue.path.map(String).join('.') || '(root)'}: ${issue.message}` : 'unknown issue'}`);
66
+ }
67
+ return parsed.data;
68
+ }
69
+ /* ------------------------------------------------------------------ *
70
+ * One fence write at a time per services instance (the tool is idempotent; two concurrent ensures must not
71
+ * race a create against an update). Across instances/processes the singleton is Coinbase's: a duplicate create
72
+ * is refused upstream and handled by the WRITE ONCE rule above.
73
+ * ------------------------------------------------------------------ */
74
+ const FENCE_LOCKS = new WeakMap();
75
+ function serializedFence(services, op) {
76
+ const prev = FENCE_LOCKS.get(services) ?? Promise.resolve();
77
+ const run = prev.then(op, op);
78
+ FENCE_LOCKS.set(services, run.catch(() => undefined));
79
+ return run;
80
+ }
81
+ function projectScoped(policies) {
82
+ return policies.filter((policy) => policy.scope === 'project');
83
+ }
84
+ function describe(policy) {
85
+ return `"${policy.description ?? '(no description)'}" (id ${policy.id}, rules digest ${policyRulesDigest(policy.rules).slice(0, 16)}…)`;
86
+ }
87
+ function refuseAmbiguous(project) {
88
+ return refuse('DELEGATION_FENCE_AMBIGUOUS', `${project.length} project-scope policies read back where Coinbase allows one — ${project.map(describe).join('; ')}; nothing written, nothing deleted; resolve by hand`);
89
+ }
90
+ function refuseForeign(policy) {
91
+ return refuse('DELEGATION_FENCE_FOREIGN', `a different project-scope policy is installed — ${describe(policy)}; Coinbase allows one, and this tool never overwrites or deletes a policy outside the "${MODEL_B_POLICY_LINEAGE_PREFIX} v<N>" lineage — remove it by hand first`);
92
+ }
93
+ function refuseNewer(policy) {
94
+ return refuse('DELEGATION_FENCE_NEWER', `the installed fence is ${describe(policy)} — a NEWER version than this server knows (v${MODEL_B_POLICY_VERSION}); refusing to downgrade it; deploy the newer server or roll the fence back by hand`);
95
+ }
96
+ /** Coinbase refused a second project-scope policy: `already_exists` (409). */
97
+ function isCreateConflict(error) {
98
+ const e = error;
99
+ return e?.statusCode === 409 || e?.errorType === 'already_exists';
100
+ }
101
+ /** `otto_delegation_fence` — ensure-by-digest + read-back bound to the write; the only writer of the project policy. */
102
+ export async function ensureDelegationFence(runtime, rawInput, caller, wait = {}) {
103
+ const services = servicesOf(runtime);
104
+ assertDelegationCaller(services, caller);
105
+ parseWith(DELEGATION_FENCE_INPUT_SCHEMA, rawInput);
106
+ return serializedFence(services, async () => {
107
+ // CARDINALITY first: more than one project policy is never judged, never written to.
108
+ const snapshot = await services.rig.listProjectPolicies();
109
+ const project = projectScoped(snapshot);
110
+ if (project.length > 1)
111
+ return refuseAmbiguous(project);
112
+ const before = readBackModelBFence(snapshot);
113
+ if (before.present)
114
+ return fenceResult('unchanged', before.id, before.rulesDigest);
115
+ let action;
116
+ let targetId;
117
+ if (project.length === 0) {
118
+ try {
119
+ targetId = await services.rig.createProjectPolicy(modelBProjectPolicyBody());
120
+ action = 'created';
121
+ }
122
+ catch (error) {
123
+ if (!isCreateConflict(error))
124
+ throw error;
125
+ // WRITE ONCE: Coinbase's singleton refused a second policy — one appeared between the snapshot and the
126
+ // write. Never a second create, never an update from this path: re-read once and judge what is there.
127
+ const again = await services.rig.listProjectPolicies();
128
+ const againProject = projectScoped(again);
129
+ if (againProject.length > 1)
130
+ return refuseAmbiguous(againProject);
131
+ const read = readBackModelBFence(again);
132
+ if (read.present)
133
+ return fenceResult('unchanged', read.id, read.rulesDigest);
134
+ const appeared = againProject[0];
135
+ if (!appeared)
136
+ return refuse('DELEGATION_FENCE_ABSENT', 'the create was refused as a duplicate but no project policy reads back; run the fence tool again');
137
+ const appearedVersion = modelBLineageVersion(appeared.description);
138
+ if (appearedVersion === undefined)
139
+ return refuseForeign(appeared);
140
+ if (appearedVersion > MODEL_B_POLICY_VERSION)
141
+ return refuseNewer(appeared);
142
+ return refuse('DELEGATION_FENCE_ABSENT', `a policy of our lineage appeared during the ensure and does not read back as v-current (${describe(appeared)}); nothing written — run the fence tool again`);
143
+ }
144
+ }
145
+ else {
146
+ const existing = project[0];
147
+ const version = modelBLineageVersion(existing.description);
148
+ if (version === undefined)
149
+ return refuseForeign(existing);
150
+ if (version > MODEL_B_POLICY_VERSION)
151
+ return refuseNewer(existing);
152
+ // BEFORE AN UPDATE: re-read this policy by id; it must still be exactly the snapshot. (No conditional
153
+ // update exists in the SDK — see the header for the operational premise that closes the remaining window.)
154
+ const current = await services.rig.readProjectPolicy(existing.id);
155
+ if (!current ||
156
+ current.scope !== 'project' ||
157
+ current.description !== existing.description ||
158
+ policyRulesDigest(current.rules) !== policyRulesDigest(existing.rules)) {
159
+ if (current && modelBLineageVersion(current.description) === undefined)
160
+ return refuseForeign(current);
161
+ return refuse('DELEGATION_FENCE_ABSENT', `the project policy changed between the snapshot and the write (${current ? describe(current) : 'it no longer reads back by id'}); nothing written — run the fence tool again`);
162
+ }
163
+ await services.rig.updateProjectPolicy(existing.id, modelBProjectPolicyBody());
164
+ targetId = existing.id;
165
+ action = 'updated';
166
+ }
167
+ // READ-BACK BOUND TO THE WRITE: exactly one project policy, its id IS the written id, and it is v-current.
168
+ // Bounded: the engine propagates asynchronously.
169
+ const pollMs = wait.pollMs ?? DEFAULT_FENCE_WAIT.pollMs;
170
+ const maxPolls = wait.maxPolls ?? DEFAULT_FENCE_WAIT.maxPolls;
171
+ let last = 'no project policy read back';
172
+ for (let poll = 0; poll < maxPolls; poll += 1) {
173
+ const listed = await services.rig.listProjectPolicies();
174
+ const afterProject = projectScoped(listed);
175
+ if (afterProject.length > 1)
176
+ return refuseAmbiguous(afterProject);
177
+ const after = readBackModelBFence(listed);
178
+ if (after.present && after.id === targetId)
179
+ return fenceResult(action, after.id, after.rulesDigest);
180
+ last = after.present
181
+ ? `a v-current policy reads back under id ${after.id}, not the written id ${targetId}`
182
+ : `${after.reason}: ${after.detail}`;
183
+ if (poll + 1 < maxPolls)
184
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
185
+ }
186
+ return refuse('DELEGATION_FENCE_ABSENT', `the fence was ${action} (id ${targetId}) but does not read back as "${MODEL_B_POLICY_NAME_V1}" with the v1 rules digest under that id after ${maxPolls} polls (${last}); nothing is submittable until it does`);
187
+ });
188
+ }
189
+ function fenceResult(action, id, rulesDigest) {
190
+ return Object.freeze({
191
+ status: 'present',
192
+ action,
193
+ policy_id: id,
194
+ policy_name: MODEL_B_POLICY_NAME_V1,
195
+ rules_digest: rulesDigest,
196
+ per_swap_cap_usd: MODEL_B_PER_SWAP_CAP_USD,
197
+ });
198
+ }
199
+ /**
200
+ * `otto_delegation_fence_status` — PUBLIC, read-only, no caller check: the fence's public promise. Nothing here
201
+ * names a user, a grant, an address or a policy's contents; `reason` is a class from `readBackModelBFence`.
202
+ */
203
+ export async function readDelegationFenceStatus(runtime) {
204
+ const services = servicesOf(runtime);
205
+ const fence = readBackModelBFence(await services.rig.listProjectPolicies());
206
+ return Object.freeze({
207
+ present: fence.present,
208
+ policy_name: MODEL_B_POLICY_NAME_V1,
209
+ per_swap_cap_usd: MODEL_B_PER_SWAP_CAP_USD,
210
+ // The public authority binding: published present or absent, so the agent CLI mints to THIS project
211
+ // (and can still bind status/revoke to it after the fence is removed). Registration requires it set.
212
+ ...(services.publicProjectId ? { project_id: services.publicProjectId } : {}),
213
+ ...(fence.present ? { rules_digest: fence.rulesDigest } : { reason: fence.reason }),
214
+ });
215
+ }
216
+ /* ------------------------------------------------------------------ *
217
+ * Identity: the user a CDP access token resolves to. The token is consulted once, never stored, never logged.
218
+ * ------------------------------------------------------------------ */
219
+ async function userFromAccessToken(services, accessToken) {
220
+ const user = await services.rig.validateAccessToken(accessToken);
221
+ if (!user)
222
+ return refuse('DELEGATION_TOKEN_INVALID', 'the access token does not resolve to a CDP end user');
223
+ return Object.freeze({
224
+ userId: user.userId,
225
+ addresses: user.evmAddresses.map((a) => getAddress(a)),
226
+ });
227
+ }
228
+ /** `otto_delegation_status` — one user's read-back, bound to the token's user. */
229
+ export async function readDelegationStatus(runtime, rawInput, caller) {
230
+ const services = servicesOf(runtime);
231
+ assertDelegationCaller(services, caller);
232
+ const input = parseWith(DELEGATION_STATUS_INPUT_SCHEMA, rawInput);
233
+ const user = await userFromAccessToken(services, input.accessToken);
234
+ const fence = readBackModelBFence(await services.rig.listProjectPolicies());
235
+ const requestedHere = revocationRequested(services.rig.rig, user.userId);
236
+ const grant = await services.rig.rig.readDelegation(user.userId);
237
+ const expiresAtMs = grant ? Date.parse(grant.expiresAt) : Number.NaN;
238
+ const active = grant !== undefined &&
239
+ !requestedHere &&
240
+ Number.isFinite(expiresAtMs) &&
241
+ expiresAtMs > runtime.nowMs();
242
+ return Object.freeze({
243
+ user_id: user.userId,
244
+ addresses: [...user.addresses],
245
+ fence: Object.freeze({
246
+ present: fence.present,
247
+ policy_name: MODEL_B_POLICY_NAME_V1,
248
+ per_swap_cap_usd: MODEL_B_PER_SWAP_CAP_USD,
249
+ ...(fence.present ? { rules_digest: fence.rulesDigest } : {}),
250
+ }),
251
+ delegation: Object.freeze({ active, ...(grant ? { expires_at: grant.expiresAt } : {}) }),
252
+ revocation_requested_here: requestedHere,
253
+ });
254
+ }
255
+ const STDOUT_SUPPORT_SINK = {
256
+ write: (entry) => process.stdout.write(`${JSON.stringify({ tag: 'delegation-support', ...entry })}\n`),
257
+ };
258
+ /** `otto_delegation_revoke` — developer-side revoke, read-back inside the rig lock, fail-closed afterwards. */
259
+ export async function revokeDelegation(runtime, rawInput, caller, supportSink = STDOUT_SUPPORT_SINK) {
260
+ const services = servicesOf(runtime);
261
+ assertDelegationCaller(services, caller);
262
+ const input = parseWith(DELEGATION_REVOKE_INPUT_SCHEMA, rawInput);
263
+ let userId;
264
+ let path;
265
+ if ('accessToken' in input) {
266
+ userId = (await userFromAccessToken(services, input.accessToken)).userId;
267
+ path = 'user';
268
+ }
269
+ else {
270
+ userId = input.userId;
271
+ path = 'support';
272
+ supportSink.write({
273
+ action: 'delegation_revoke',
274
+ userId,
275
+ // Free text from an operator: URLs and any configured secret value are redacted before it lands in a log.
276
+ reason: redactBootMessage(input.support_reason),
277
+ ts: new Date(runtime.nowMs()).toISOString(),
278
+ });
279
+ }
280
+ try {
281
+ // Serialized on the rig, the lock held through the read-back; the user is marked revocation-requested BEFORE
282
+ // the raw revoke, so a plan queued behind this call is refused whatever the read-back says.
283
+ await services.rig.rig.revoke(userId);
284
+ }
285
+ catch {
286
+ return refuse('DELEGATION_REVOKE_NOT_CONFIRMED', 'the revoke was issued but the delegation still reads back; this server submits nothing for the user from now on — retry, and check the CDP portal');
287
+ }
288
+ return Object.freeze({ user_id: userId, revoked: true, reads_back: false, path });
289
+ }
290
+ //# sourceMappingURL=execution-delegation-admin.js.map
@@ -0,0 +1,257 @@
1
+ /**
2
+ * execution-delegation-policy.ts — the product's CDP project policy that fences the delegated leg
3
+ * (Model B, browser plane; seat ruling D9 = α+, 2026-08-29).
4
+ *
5
+ * WHAT THE ENGINE FENCES, AND WHAT IT CANNOT. Coinbase's policy engine offers END-USER accounts exactly
6
+ * one fence: the project-scope policy ("Only one project-level policy can exist at any time"; account-
7
+ * scoped policies attach only to developer-owned accounts, and no rule can refer to "the sender's own
8
+ * address"). So this policy is a SHAPE fence with constant caps, shared by every delegated user:
9
+ * - approve: only on an allow-listed input token, only to the reviewed LI.FI diamond, only up to the
10
+ * per-swap cap (5,000 USD, Founder word 2026-08-29) in that token's atomic units — a bound on ONE
11
+ * swap, not a daily or total limit (zero stays allowed for the allowance clear);
12
+ * - call: only the LI.FI diamond, zero native value, only the two GenericSwapV3 ERC20→ERC20 entry points.
13
+ * The approve cap is what bounds the swap's notional on-chain: the diamond can only pull what is
14
+ * approved. Everything else — that `_receiver` is the end user, that `_minAmount` is at least the
15
+ * caller's floor, that the fee leg is the ruled attribution — is Otto's SERVER-SIDE 13-check verifier on
16
+ * every call (`adapter/verify.ts`), run with the end user's address as the server reads it from CDP.
17
+ *
18
+ * THE RESIDUAL, STATED PLAINLY (also in the PR body and in the consent copy the user reads):
19
+ * Coinbase's engine limits Otto to swaps through LI.FI up to the per-swap cap; Otto's own server checks
20
+ * where the output goes; the user can revoke at any time. The engine does NOT bind the receiver — a
21
+ * compromised Otto server could re-point `_receiver` inside a diamond swap; that residual is bounded by
22
+ * the per-swap cap, the grant's expiry and the user's revoke, not by the engine.
23
+ *
24
+ * IDENTITY OF THE FENCE (seat ruling D6): the policy is matched by its versioned DESCRIPTION and checked
25
+ * by a digest over its canonical rules on every read-back — never by a policy id in env (a re-created
26
+ * policy silently changes id; a renamed one silently changes shape; the digest catches both). The
27
+ * canonical form lowercases hex strings and sorts object keys, nothing else; if the engine ever
28
+ * augments rules on read-back, `canonical()` is the one place to teach it.
29
+ *
30
+ * Description obeys the SDK's regex (`/^[A-Za-z0-9 ,.]{1,50}$/`): no hyphens, no parentheses.
31
+ */
32
+ import { type Address } from 'viem';
33
+ /**
34
+ * The LINEAGE: a project policy is Otto's own only when its description matches the ANCHORED pattern
35
+ * `^OTTO model b swap fence v<N>$` (`modelBLineageVersion` returns N). The fence tool updates a lineage policy
36
+ * in place only when N ≤ this binary's version; a newer N is never downgraded; anything that does not match —
37
+ * the prefix in the middle, a suffix, no description — is foreign and is never touched.
38
+ */
39
+ export declare const MODEL_B_POLICY_LINEAGE_PREFIX = "OTTO model b swap fence";
40
+ /** This binary's fence version. Bump it whenever the rules change shape or caps. */
41
+ export declare const MODEL_B_POLICY_VERSION: 1;
42
+ /** The versioned match key. */
43
+ export declare const MODEL_B_POLICY_NAME_V1: string;
44
+ /** The lineage version N of a policy description, or undefined when the description is not of our lineage. */
45
+ export declare function modelBLineageVersion(description: unknown): number | undefined;
46
+ export declare const MODEL_B_POLICY_NETWORK: "base";
47
+ export declare const MODEL_B_CHAIN_ID: 8453;
48
+ /**
49
+ * Delegation expiry bounds (Founder word `expiry 90/180`, 2026-08-29): the mint page defaults to 90 days
50
+ * and refuses anything past 180; the server routes enforce the same numbers. Coinbase documents no
51
+ * maximum of its own for `expiresAt` (SDK types + vendor doc read 2026-08-29); the first 180-day mint
52
+ * at the sitting is the live confirmation.
53
+ */
54
+ export declare const MODEL_B_DELEGATION_EXPIRY_DEFAULT_DAYS: 90;
55
+ export declare const MODEL_B_DELEGATION_EXPIRY_MAX_DAYS: 180;
56
+ /** A requested expiry, bounded: a hostile, absent or over-long value never reaches the mint. */
57
+ export declare function boundedDelegationExpiry(requestedDays: unknown, nowMs: number): {
58
+ readonly days: number;
59
+ readonly expiresAt: string;
60
+ };
61
+ /**
62
+ * The per-swap cap in USD — the Founder's number (word `fence cap 5000`, 2026-08-29). It bounds ONE swap:
63
+ * the engine carries no daily or total limit, and no copy may imply one. Changing it = a new fence version.
64
+ * Engine conditions are on raw calldata, so the cap is expressed per allowed input token: v1 allows USDC
65
+ * only, where the mapping is exact (5,000 USD = 5,000,000,000 atomic on the approve amount, which is what
66
+ * bounds the swap's `fromAmount` on-chain).
67
+ */
68
+ export declare const MODEL_B_PER_SWAP_CAP_USD: 5000;
69
+ export interface ModelBInputToken {
70
+ readonly symbol: string;
71
+ readonly address: Address;
72
+ readonly decimals: number;
73
+ /** Atomic units per one USD — exact for a USD stablecoin; anything else needs a ruled reference price. */
74
+ readonly atomicPerUsd: bigint;
75
+ }
76
+ export declare const BASE_USDC: Address;
77
+ /**
78
+ * The input tokens a delegated swap may spend, v1: USDC only — the one token whose USD cap is exact.
79
+ * WETH (or any volatile input) needs a Founder-ruled reference price before it can carry a USD cap.
80
+ */
81
+ export declare const MODEL_B_INPUT_TOKENS_V1: readonly ModelBInputToken[];
82
+ export declare function modelBInputToken(token: Address): ModelBInputToken | undefined;
83
+ /** The cap in the token's atomic units. */
84
+ export declare function modelBPerSwapCapAtomic(token: ModelBInputToken, capUsd?: number): bigint;
85
+ /** The two GenericSwapV3 ERC20→ERC20 entry points a same-chain swap plan may call. */
86
+ export declare const GENERIC_SWAP_V3_ABI: readonly [{
87
+ readonly type: "function";
88
+ readonly name: "swapTokensSingleV3ERC20ToERC20";
89
+ readonly stateMutability: "nonpayable";
90
+ readonly inputs: readonly [{
91
+ readonly name: "_transactionId";
92
+ readonly type: "bytes32";
93
+ }, {
94
+ readonly name: "_integrator";
95
+ readonly type: "string";
96
+ }, {
97
+ readonly name: "_referrer";
98
+ readonly type: "string";
99
+ }, {
100
+ readonly name: "_receiver";
101
+ readonly type: "address";
102
+ }, {
103
+ readonly name: "_minAmount";
104
+ readonly type: "uint256";
105
+ }, {
106
+ readonly name: "_swapData";
107
+ readonly type: "tuple";
108
+ readonly components: readonly [{
109
+ readonly name: "callTo";
110
+ readonly type: "address";
111
+ }, {
112
+ readonly name: "approveTo";
113
+ readonly type: "address";
114
+ }, {
115
+ readonly name: "sendingAssetId";
116
+ readonly type: "address";
117
+ }, {
118
+ readonly name: "receivingAssetId";
119
+ readonly type: "address";
120
+ }, {
121
+ readonly name: "fromAmount";
122
+ readonly type: "uint256";
123
+ }, {
124
+ readonly name: "callData";
125
+ readonly type: "bytes";
126
+ }, {
127
+ readonly name: "requiresDeposit";
128
+ readonly type: "bool";
129
+ }];
130
+ }];
131
+ readonly outputs: readonly [];
132
+ }, {
133
+ readonly type: "function";
134
+ readonly name: "swapTokensMultipleV3ERC20ToERC20";
135
+ readonly stateMutability: "nonpayable";
136
+ readonly inputs: readonly [{
137
+ readonly name: "_transactionId";
138
+ readonly type: "bytes32";
139
+ }, {
140
+ readonly name: "_integrator";
141
+ readonly type: "string";
142
+ }, {
143
+ readonly name: "_referrer";
144
+ readonly type: "string";
145
+ }, {
146
+ readonly name: "_receiver";
147
+ readonly type: "address";
148
+ }, {
149
+ readonly name: "_minAmount";
150
+ readonly type: "uint256";
151
+ }, {
152
+ readonly name: "_swapData";
153
+ readonly type: "tuple[]";
154
+ readonly components: readonly [{
155
+ readonly name: "callTo";
156
+ readonly type: "address";
157
+ }, {
158
+ readonly name: "approveTo";
159
+ readonly type: "address";
160
+ }, {
161
+ readonly name: "sendingAssetId";
162
+ readonly type: "address";
163
+ }, {
164
+ readonly name: "receivingAssetId";
165
+ readonly type: "address";
166
+ }, {
167
+ readonly name: "fromAmount";
168
+ readonly type: "uint256";
169
+ }, {
170
+ readonly name: "callData";
171
+ readonly type: "bytes";
172
+ }, {
173
+ readonly name: "requiresDeposit";
174
+ readonly type: "bool";
175
+ }];
176
+ }];
177
+ readonly outputs: readonly [];
178
+ }];
179
+ /** The rule dialect this fence emits — structurally the SDK's `sendEndUserEvmTransaction` rule. */
180
+ export interface DelegationPolicyParamCondition {
181
+ readonly name: string;
182
+ readonly operator: 'in' | '<=' | '>=';
183
+ readonly value?: string;
184
+ readonly values?: readonly string[];
185
+ }
186
+ export interface DelegationPolicyDataCondition {
187
+ readonly function: string;
188
+ readonly params?: readonly DelegationPolicyParamCondition[];
189
+ }
190
+ export type DelegationPolicyCriterion = {
191
+ readonly type: 'evmNetwork';
192
+ readonly networks: readonly string[];
193
+ readonly operator: 'in';
194
+ } | {
195
+ readonly type: 'evmAddress';
196
+ readonly addresses: readonly string[];
197
+ readonly operator: 'in';
198
+ } | {
199
+ readonly type: 'ethValue';
200
+ readonly ethValue: string;
201
+ readonly operator: '<=';
202
+ } | {
203
+ readonly type: 'evmData';
204
+ readonly abi: 'erc20' | typeof GENERIC_SWAP_V3_ABI;
205
+ readonly conditions: readonly DelegationPolicyDataCondition[];
206
+ };
207
+ export interface DelegationPolicyRule {
208
+ readonly action: 'accept';
209
+ readonly operation: 'sendEndUserEvmTransaction';
210
+ readonly criteria: readonly DelegationPolicyCriterion[];
211
+ }
212
+ /** The static v1 rules: one approve rule per allow-listed input token, one router rule. */
213
+ export declare function modelBProjectPolicyRules(capUsd?: number): readonly DelegationPolicyRule[];
214
+ /** The body PR-2's installer creates or updates: scope, versioned description, the v1 rules. */
215
+ export declare function modelBProjectPolicyBody(): {
216
+ readonly scope: 'project';
217
+ readonly description: string;
218
+ readonly rules: readonly DelegationPolicyRule[];
219
+ };
220
+ /** SHA-256 over the canonical JSON of a rule list — the fence's integrity check at read-back. */
221
+ export declare function policyRulesDigest(rules: unknown): string;
222
+ export declare const MODEL_B_POLICY_DIGEST_V1: string;
223
+ /** What the server reads back from `cdp.policies.listPolicies({ scope: 'project' })`, structurally. */
224
+ export interface DelegationPolicyRecord {
225
+ readonly id: string;
226
+ readonly description?: string;
227
+ readonly scope: string;
228
+ readonly rules: unknown;
229
+ }
230
+ export type ModelBFenceReadBack = {
231
+ readonly present: true;
232
+ readonly id: string;
233
+ readonly rulesDigest: string;
234
+ } | {
235
+ readonly present: false;
236
+ readonly reason: 'absent' | 'ambiguous' | 'name_mismatch' | 'digest_mismatch';
237
+ readonly detail: string;
238
+ };
239
+ /**
240
+ * The fence check the delegated-send path runs BEFORE any send: EXACTLY ONE project-scope policy reads back
241
+ * (Coinbase allows one; more is an anomalous snapshot and never blessed on the strength of "one of them
242
+ * matches"), it carries the versioned name, and its rules digest to exactly the v1 rules. Anything else is
243
+ * "absent" — including a policy of the right name whose rules drifted.
244
+ */
245
+ export declare function readBackModelBFence(policies: readonly DelegationPolicyRecord[]): ModelBFenceReadBack;
246
+ /**
247
+ * The same bound the engine enforces, checked server-side BEFORE the first step is sent: a plan the
248
+ * engine would refuse is refused here without spending an allowance reset on it.
249
+ */
250
+ export declare function assertWithinModelBFence(plan: {
251
+ readonly token: Address;
252
+ readonly spender: Address;
253
+ readonly gross: bigint;
254
+ }, capUsd?: number, inputTokens?: readonly ModelBInputToken[]): {
255
+ readonly token: ModelBInputToken;
256
+ readonly capAtomic: bigint;
257
+ };