@crediolabs/policy-synth 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/oz/adapter.js +18 -18
- package/dist/mandate/to-ir.js +4 -4
- package/dist/mandate/types.d.ts +1 -1
- package/dist/seams/types.d.ts +1 -1
- package/dist/synth/compose-from-recording.d.ts +1 -1
- package/dist/synth/compose-from-recording.js +12 -12
- package/dist/synth/lower.d.ts +2 -2
- package/dist/synth/synthesize-from-mandate.js +5 -5
- package/dist/synth/synthesize-from-recording.js +5 -5
- package/package.json +1 -1
- package/src/adapters/oz/adapter.ts +18 -18
- package/src/mandate/to-ir.ts +4 -4
- package/src/mandate/types.ts +1 -1
- package/src/seams/types.ts +1 -1
- package/src/synth/compose-from-recording.ts +13 -13
- package/src/synth/lower.ts +2 -2
- package/src/synth/synthesize-from-mandate.ts +5 -5
- package/src/synth/synthesize-from-recording.ts +5 -5
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// src/adapters/oz/adapter.ts - the OZ Accounts CustodyAdapter
|
|
1
|
+
// src/adapters/oz/adapter.ts - the OZ Accounts CustodyAdapter.
|
|
2
2
|
//
|
|
3
3
|
// Compiles a PolicyIR to an OZ `ProposedPolicy` using OZ built-in policy
|
|
4
|
-
// primitives
|
|
4
|
+
// primitives. Only the constructs OZ can express natively are lowered:
|
|
5
5
|
// scope.contract -> ContextRuleType.call_contract (else default)
|
|
6
6
|
// expiry.validUntilLedger -> ContextRuleDraft.validUntilLedger
|
|
7
7
|
// window_spent(t,w) <= L -> `spending_limit` primitive
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// Anything needing a capability this backend lacks (oracle price, invocation
|
|
10
10
|
// count, per-arg comparison/allowlist, guard, nested boolean predicate) is NOT
|
|
11
11
|
// emitted: it is named in `uncovered` and `covered` is set false. Nothing is
|
|
12
|
-
// silently dropped - the uncovered constructs are
|
|
12
|
+
// silently dropped - the uncovered constructs are named instead.
|
|
13
13
|
//
|
|
14
14
|
// OZ built-in policy instance addresses are per-network deploy artifacts we do
|
|
15
15
|
// not have yet (install is a later phase). They are injected via config; week-1
|
|
@@ -84,8 +84,8 @@ function lowerRule(rule, config) {
|
|
|
84
84
|
const uncovered = [];
|
|
85
85
|
const policyRefs = [];
|
|
86
86
|
// scope -> context rule type. OZ scopes by contract (CallContract); a finer
|
|
87
|
-
// method-level restriction is a predicate concern and must be flagged
|
|
88
|
-
// because CallContract alone permits other methods on the same contract
|
|
87
|
+
// method-level restriction is a predicate concern and must be flagged as not
|
|
88
|
+
// covered because CallContract alone permits other methods on the same contract
|
|
89
89
|
// (e.g. an unbounded approve alongside a capped transfer).
|
|
90
90
|
const contextRuleType = rule.scope.contract !== undefined
|
|
91
91
|
? { kind: 'call_contract', contract: rule.scope.contract }
|
|
@@ -110,11 +110,11 @@ function lowerRule(rule, config) {
|
|
|
110
110
|
uncovered.push('time expiry given as a unix timestamp (OZ context rules expire by ledger sequence; supply expiry.validUntilLedger)');
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
|
-
// guard ->
|
|
113
|
+
// guard -> not covered (applicability predicates are not an OZ built-in).
|
|
114
114
|
if (rule.guard) {
|
|
115
115
|
uncovered.push(`guard: ${describeCondition(rule.guard)}`);
|
|
116
116
|
}
|
|
117
|
-
// constraints -> spending_limit where they match; else
|
|
117
|
+
// constraints -> spending_limit where they match; else not covered. The OZ
|
|
118
118
|
// spending_limit policy takes `{ spending_limit: i128, period_ledgers: u32 }`
|
|
119
119
|
// and has NO token param: it only accepts a CallContract context rule
|
|
120
120
|
// (OnlyCallContractAllowed) and limits transfers of that context's contract,
|
|
@@ -144,7 +144,7 @@ function lowerRule(rule, config) {
|
|
|
144
144
|
}
|
|
145
145
|
// approval.threshold -> simple/weighted threshold primitive. A threshold < 1
|
|
146
146
|
// is not a real M-of-N gate (0 approvals authorises everything), so refuse to
|
|
147
|
-
// emit a no-op primitive and flag it
|
|
147
|
+
// emit a no-op primitive and flag it as not covered instead.
|
|
148
148
|
if (rule.approval) {
|
|
149
149
|
if (!Number.isInteger(rule.approval.threshold) || rule.approval.threshold < 1) {
|
|
150
150
|
uncovered.push(`approval threshold ${rule.approval.threshold} is not a positive integer (a 0 or negative threshold is not an M-of-N gate)`);
|
|
@@ -203,17 +203,17 @@ function matchSpendingLimit(c) {
|
|
|
203
203
|
return null;
|
|
204
204
|
return { token: selector.token, limit: value, windowSeconds: selector.windowSeconds };
|
|
205
205
|
}
|
|
206
|
-
/** Human-readable descriptor for a construct the OZ
|
|
207
|
-
* express, used to populate `uncovered
|
|
206
|
+
/** Human-readable descriptor for a construct the OZ built-in-primitive backend
|
|
207
|
+
* cannot express, used to populate `uncovered`. */
|
|
208
208
|
function describeCondition(cond) {
|
|
209
209
|
switch (cond.op) {
|
|
210
210
|
case 'in':
|
|
211
|
-
return `value allowlist on ${describeSelector(cond.selector)} (arg allowlist
|
|
211
|
+
return `value allowlist on ${describeSelector(cond.selector)} (arg allowlist)`;
|
|
212
212
|
case 'not':
|
|
213
|
-
return 'negated condition (predicate DSL
|
|
213
|
+
return 'negated condition (predicate DSL)';
|
|
214
214
|
case 'and':
|
|
215
215
|
case 'or':
|
|
216
|
-
return `nested ${cond.op} condition (predicate DSL
|
|
216
|
+
return `nested ${cond.op} condition (predicate DSL)`;
|
|
217
217
|
case 'compare': {
|
|
218
218
|
const s = cond.compare.selector;
|
|
219
219
|
switch (s.kind) {
|
|
@@ -224,16 +224,16 @@ function describeCondition(cond) {
|
|
|
224
224
|
case 'window_spent':
|
|
225
225
|
return `spend-window comparison with operator '${cond.compare.operator}' (only 'lte' lowers to spending_limit)`;
|
|
226
226
|
case 'amount':
|
|
227
|
-
return `per-call amount comparison on ${s.token} (predicate DSL
|
|
227
|
+
return `per-call amount comparison on ${s.token} (predicate DSL)`;
|
|
228
228
|
case 'arg':
|
|
229
|
-
return `argument comparison on arg ${s.argIndex} (predicate DSL
|
|
229
|
+
return `argument comparison on arg ${s.argIndex} (predicate DSL)`;
|
|
230
230
|
case 'calldata':
|
|
231
|
-
return 'EVM calldata comparison (predicate DSL
|
|
231
|
+
return 'EVM calldata comparison (predicate DSL)';
|
|
232
232
|
case 'value':
|
|
233
|
-
return 'tx.value comparison (predicate DSL
|
|
233
|
+
return 'tx.value comparison (predicate DSL)';
|
|
234
234
|
case 'now':
|
|
235
235
|
case 'valid_until':
|
|
236
|
-
return 'time comparison (predicate DSL
|
|
236
|
+
return 'time comparison (predicate DSL)';
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
239
|
}
|
package/dist/mandate/to-ir.js
CHANGED
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
// contract/method -> rule.scope
|
|
7
7
|
// spendingLimit -> a `window_spent(token,window) <= limit` compare
|
|
8
8
|
// approvalThreshold-> rule.approval.threshold
|
|
9
|
-
// recipients -> an `in` condition on the recipient arg (
|
|
10
|
-
// by the OZ adapter; that is expected)
|
|
9
|
+
// recipients -> an `in` condition on the recipient arg (flagged as not
|
|
10
|
+
// covered by the OZ adapter; that is expected)
|
|
11
11
|
// expiry -> rule.expiry
|
|
12
12
|
// The top-level default is `deny_all` (OZ context rules are deny-by-default).
|
|
13
13
|
/** Arg index the recipient allowlist constrains. Pinned to the SEP-41
|
|
14
14
|
* `transfer(from, to, amount)` convention where `to` is arg 1. This condition
|
|
15
|
-
* is flagged
|
|
16
|
-
* allowlist), so the index only needs to be deterministic in week-1. */
|
|
15
|
+
* is flagged as not covered by the OZ adapter (no built-in primitive expresses
|
|
16
|
+
* an arg allowlist), so the index only needs to be deterministic in week-1. */
|
|
17
17
|
const RECIPIENT_ARG_INDEX = 1;
|
|
18
18
|
export function mandateToPolicyIR(spec) {
|
|
19
19
|
const scope = { contract: spec.contract };
|
package/dist/mandate/types.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export interface MandateSpec {
|
|
|
10
10
|
};
|
|
11
11
|
/** -> OZ `simple_threshold` / `weighted_threshold` primitive. */
|
|
12
12
|
approvalThreshold?: number;
|
|
13
|
-
/** Recipient allowlist -> an `in` arg condition (
|
|
13
|
+
/** Recipient allowlist -> an `in` arg condition (not covered by an OZ built-in). */
|
|
14
14
|
recipients?: string[];
|
|
15
15
|
/** -> OZ context-rule expiry. */
|
|
16
16
|
expiry?: {
|
package/dist/seams/types.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ export interface CustodyCapabilities {
|
|
|
36
36
|
export interface CompileResult {
|
|
37
37
|
/** false => some IR construct this backend cannot express (see `uncovered`). */
|
|
38
38
|
covered: boolean;
|
|
39
|
-
/** Human-readable list of unsupported constructs
|
|
39
|
+
/** Human-readable list of unsupported constructs. */
|
|
40
40
|
uncovered: string[];
|
|
41
41
|
/** The backend-native installable policy, assembled when a rule lowered. */
|
|
42
42
|
proposed?: ProposedPolicy;
|
|
@@ -22,7 +22,7 @@ export interface ComposeOptions {
|
|
|
22
22
|
userResponses?: ComposeUserResponses;
|
|
23
23
|
}
|
|
24
24
|
/** Result of composition: the PolicyIR, any ambiguities surfaced during
|
|
25
|
-
* inference, and descriptive
|
|
25
|
+
* inference, and descriptive warnings for needs that are NOT expressed
|
|
26
26
|
* as an IR node (so no fabricated constraint is emitted). The orchestrator
|
|
27
27
|
* carries ambiguities into `ProposedPolicy.ambiguities` and merges warnings
|
|
28
28
|
* into `ProposedPolicy.warnings`. */
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
// src/synth/compose-from-recording.ts - facts + scope -> PolicyIR
|
|
1
|
+
// src/synth/compose-from-recording.ts - facts + scope -> PolicyIR.
|
|
2
2
|
//
|
|
3
|
-
// Composes the canonical IR rule the OZ
|
|
3
|
+
// Composes the canonical IR rule the OZ built-in-primitive backend can compile. Fail-closed
|
|
4
4
|
// composition rules:
|
|
5
5
|
//
|
|
6
6
|
// - identify the protocol of the top-level call (registry.identifyProtocol).
|
|
7
7
|
// When it is unknown (null), emit NO OZ-primitive-producing IR node: the
|
|
8
8
|
// scope is kept (CallContract + method) and every inferred bound is surfaced
|
|
9
|
-
// as a descriptive
|
|
9
|
+
// as a descriptive warning. An unrecognised call never compiles to a
|
|
10
10
|
// permissive OZ primitive.
|
|
11
11
|
//
|
|
12
12
|
// - carry the recorded top-level function into `rule.scope.method` so the OZ
|
|
13
|
-
// adapter flags per-method scoping
|
|
14
|
-
// on the contract; a per-method restriction needs the interpreter predicate).
|
|
13
|
+
// adapter flags per-method scoping as not covered (CallContract permits every
|
|
14
|
+
// method on the contract; a per-method restriction needs the interpreter predicate).
|
|
15
15
|
//
|
|
16
16
|
// - a `spending_limit` (window_spent(token, w) <= limit) is emitted ONLY when
|
|
17
17
|
// the caller supplies BOTH the limit (userResponses.limitAmount) and the
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
// - the IR carries ONLY constraints justified by the recording (observed
|
|
31
31
|
// recipient allowlist) + explicit user input. Nothing invented: no oracle
|
|
32
32
|
// price fabricated from a slippage bound, no synthetic exact-path compare.
|
|
33
|
-
// Those needs are surfaced as descriptive
|
|
33
|
+
// Those needs are surfaced as descriptive warnings instead.
|
|
34
34
|
//
|
|
35
35
|
// The default behavior is `deny_all` (OZ context rules are deny-by-default).
|
|
36
36
|
import { identifyProtocol } from "../registry/identify.js";
|
|
@@ -57,7 +57,7 @@ export function composeFromRecording(facts, scopeContract, topLevel, opts) {
|
|
|
57
57
|
if (observed === undefined)
|
|
58
58
|
continue;
|
|
59
59
|
if (!known) {
|
|
60
|
-
warnings.push(`spend of ${observed} (token ${token}) not bounded: unrecognised protocol, spend cap needs the interpreter predicate
|
|
60
|
+
warnings.push(`spend of ${observed} (token ${token}) not bounded: unrecognised protocol, spend cap needs the interpreter predicate`);
|
|
61
61
|
continue;
|
|
62
62
|
}
|
|
63
63
|
const limit = spendTokens.length === 1 ? limitAmount : undefined;
|
|
@@ -106,11 +106,11 @@ export function composeFromRecording(facts, scopeContract, topLevel, opts) {
|
|
|
106
106
|
code: 'FREQUENCY_BOUND_MISSING',
|
|
107
107
|
question: 'Incoming-only flow - what max invocations per window should the policy enforce?',
|
|
108
108
|
});
|
|
109
|
-
warnings.push('frequency bound needed for the incoming-only flow (interpreter
|
|
109
|
+
warnings.push('frequency bound needed for the incoming-only flow (needs the interpreter predicate); no invocation cap inferred');
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
// Observed recipient allowlist (SEP-41) is a real, recorded constraint the OZ
|
|
113
|
-
// adapter flags
|
|
113
|
+
// adapter flags as not covered. Unknown protocols emit nothing here.
|
|
114
114
|
if (topLevel && protocol !== null) {
|
|
115
115
|
appendProtocolSpecificConstraints(constraints, warnings, facts, topLevel, protocol);
|
|
116
116
|
}
|
|
@@ -134,11 +134,11 @@ export function composeFromRecording(facts, scopeContract, topLevel, opts) {
|
|
|
134
134
|
}
|
|
135
135
|
/** Add the constraints justified by the recording that the OZ backend cannot
|
|
136
136
|
* express natively (the SEP-41 recipient the transfer moved to). The OZ adapter
|
|
137
|
-
* flags them
|
|
137
|
+
* flags them as not covered. SoroSwap's slippage / oracle / exact-path needs are
|
|
138
138
|
* surfaced as descriptive warnings, NOT as fabricated comparison nodes. */
|
|
139
139
|
function appendProtocolSpecificConstraints(constraints, warnings, facts, topLevel, protocol) {
|
|
140
140
|
// SEP-41 transfer / mint: the `to` arg (index 1) is the recipient. Emit it as
|
|
141
|
-
// a single-element allowlist; the OZ adapter flags it
|
|
141
|
+
// a single-element allowlist; the OZ adapter flags it as not covered.
|
|
142
142
|
if (protocol.protocol === 'sep41' && (protocol.fn === 'transfer' || protocol.fn === 'mint')) {
|
|
143
143
|
const toArg = topLevel.args[1];
|
|
144
144
|
if (toArg && toArg.type === 'address') {
|
|
@@ -157,6 +157,6 @@ function appendProtocolSpecificConstraints(constraints, warnings, facts, topLeve
|
|
|
157
157
|
const paths = facts.allowedPaths?.[topLevel.contract];
|
|
158
158
|
const route = paths?.length === 1 ? paths[0] : undefined;
|
|
159
159
|
const pathText = route && route.length > 0 ? `; observed path: ${route.join(' -> ')}` : '';
|
|
160
|
-
warnings.push(`SoroSwap swap: slippage / oracle price bound and exact hop path need the interpreter predicate
|
|
160
|
+
warnings.push(`SoroSwap swap: slippage / oracle price bound and exact hop path need the interpreter predicate${pathText}`);
|
|
161
161
|
}
|
|
162
162
|
}
|
package/dist/synth/lower.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export interface IntentFacts {
|
|
|
4
4
|
callTargets: string[];
|
|
5
5
|
functionsByContract: Record<string, string[]>;
|
|
6
6
|
/** Aggregate outgoing spend per token (i128-safe decimal string). A single
|
|
7
|
-
* token entry is the
|
|
7
|
+
* token entry is the prerequisite for `spending_limit`. Movements
|
|
8
8
|
* with from != sourceAccount are NOT counted as "spend" by this synth
|
|
9
9
|
* (incoming yield, refund, etc.). */
|
|
10
10
|
spendByToken: Record<string, string>;
|
|
@@ -15,7 +15,7 @@ export interface IntentFacts {
|
|
|
15
15
|
sharedRouter?: string;
|
|
16
16
|
/** Router hop paths, keyed by target contract. SoroSwap exposes its `path`
|
|
17
17
|
* arg directly on the top-level call; this is the input the compose step
|
|
18
|
-
* passes to a per-arg `in` condition (flagged
|
|
18
|
+
* passes to a per-arg `in` condition (flagged as not covered by the OZ adapter). */
|
|
19
19
|
allowedPaths?: Record<string, string[][]>;
|
|
20
20
|
}
|
|
21
21
|
/** Lower a recorded transaction to the canonical IntentFacts. Pure (no
|
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
// MandateSpec is lowered deterministically to a PolicyIR and compiled by the OZ
|
|
5
5
|
// adapter to a ProposedPolicy. No decoding, no inference, so parseConfidence is
|
|
6
6
|
// the full/not-applicable value. Constructs the OZ built-in primitives cannot
|
|
7
|
-
// express (compile's `uncovered`) are surfaced in `ProposedPolicy.warnings`
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// express (compile's `uncovered`) are surfaced in `ProposedPolicy.warnings`
|
|
8
|
+
// rather than failing the call - the covered primitives still install; the
|
|
9
|
+
// uncovered ones are reported.
|
|
10
10
|
import { createOzAdapter } from "../adapters/oz/adapter.js";
|
|
11
11
|
import { mandateToPolicyIR } from "../mandate/to-ir.js";
|
|
12
|
-
const
|
|
12
|
+
const UNCOVERED_PREFIX = 'Not covered by OZ built-in primitives: ';
|
|
13
13
|
export function synthesizeFromMandate(spec, ozConfig) {
|
|
14
14
|
const ir = mandateToPolicyIR(spec);
|
|
15
15
|
const adapter = createOzAdapter(ozConfig);
|
|
@@ -28,7 +28,7 @@ export function synthesizeFromMandate(spec, ozConfig) {
|
|
|
28
28
|
}
|
|
29
29
|
const proposed = {
|
|
30
30
|
...result.proposed,
|
|
31
|
-
warnings: result.uncovered.map((u) => `${
|
|
31
|
+
warnings: result.uncovered.map((u) => `${UNCOVERED_PREFIX}${u}`),
|
|
32
32
|
};
|
|
33
33
|
return { ok: true, data: proposed };
|
|
34
34
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// 4. `composeFromRecording(facts, scope, opts)` -> PolicyIR + ambiguities.
|
|
12
12
|
// 5. `ozAdapter.compile(ir)` -> CompileResult.
|
|
13
13
|
// 6. Assemble ProposedPolicy carrying parseConfidence (from tx), warnings
|
|
14
|
-
// (the OZ `uncovered`
|
|
14
|
+
// (the OZ `uncovered` markers, prefixed), and ambiguities
|
|
15
15
|
// (DURATION_UNSPECIFIED etc.).
|
|
16
16
|
//
|
|
17
17
|
// Determinism: same (tx, opts, ozConfig) -> byte-identical ProposedPolicy.
|
|
@@ -21,7 +21,7 @@ import { SOROBAN_LIMITS, } from "../types.js";
|
|
|
21
21
|
import { composeFromRecording, } from "./compose-from-recording.js";
|
|
22
22
|
import { lower } from "./lower.js";
|
|
23
23
|
import { decideScope } from "./scope.js";
|
|
24
|
-
const
|
|
24
|
+
const UNCOVERED_PREFIX = 'Not covered by OZ built-in primitives: ';
|
|
25
25
|
/** Synthesize a ProposedPolicy from a recorded transaction. */
|
|
26
26
|
export function synthesizeFromRecording(tx, opts, ozConfig) {
|
|
27
27
|
// 0. validate inputs (fail closed - never synthesize from garbage).
|
|
@@ -68,7 +68,7 @@ export function synthesizeFromRecording(tx, opts, ozConfig) {
|
|
|
68
68
|
return scopeRes;
|
|
69
69
|
const scope = scopeRes.data;
|
|
70
70
|
if (scope.kind !== 'call_contract') {
|
|
71
|
-
// A default scope on the recording-path is not currently a
|
|
71
|
+
// A default scope on the recording-path is not currently a covered flow;
|
|
72
72
|
// surface SYNTHESIS_ERROR so the orchestrator never silently emits an
|
|
73
73
|
// over-broad default-scoped policy.
|
|
74
74
|
return {
|
|
@@ -108,8 +108,8 @@ export function synthesizeFromRecording(tx, opts, ozConfig) {
|
|
|
108
108
|
...compileRes.proposed,
|
|
109
109
|
parseConfidence: { ...tx.parseConfidence },
|
|
110
110
|
warnings: [
|
|
111
|
-
...compileRes.uncovered.map((u) => `${
|
|
112
|
-
...composed.warnings.map((w) => `${
|
|
111
|
+
...compileRes.uncovered.map((u) => `${UNCOVERED_PREFIX}${u}`),
|
|
112
|
+
...composed.warnings.map((w) => `${UNCOVERED_PREFIX}${w}`),
|
|
113
113
|
],
|
|
114
114
|
ambiguities: mergeAmbiguities(composed.ambiguities, scope.ambiguities),
|
|
115
115
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crediolabs/policy-synth",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Off-chain TypeScript synthesis core for the OZ Accounts Policy Builder. Records Soroban transactions, synthesises the minimal policy that permits exactly that flow, verifies it, and returns an unsigned install transaction.",
|
|
6
6
|
"type": "module",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// src/adapters/oz/adapter.ts - the OZ Accounts CustodyAdapter
|
|
1
|
+
// src/adapters/oz/adapter.ts - the OZ Accounts CustodyAdapter.
|
|
2
2
|
//
|
|
3
3
|
// Compiles a PolicyIR to an OZ `ProposedPolicy` using OZ built-in policy
|
|
4
|
-
// primitives
|
|
4
|
+
// primitives. Only the constructs OZ can express natively are lowered:
|
|
5
5
|
// scope.contract -> ContextRuleType.call_contract (else default)
|
|
6
6
|
// expiry.validUntilLedger -> ContextRuleDraft.validUntilLedger
|
|
7
7
|
// window_spent(t,w) <= L -> `spending_limit` primitive
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// Anything needing a capability this backend lacks (oracle price, invocation
|
|
10
10
|
// count, per-arg comparison/allowlist, guard, nested boolean predicate) is NOT
|
|
11
11
|
// emitted: it is named in `uncovered` and `covered` is set false. Nothing is
|
|
12
|
-
// silently dropped - the uncovered constructs are
|
|
12
|
+
// silently dropped - the uncovered constructs are named instead.
|
|
13
13
|
//
|
|
14
14
|
// OZ built-in policy instance addresses are per-network deploy artifacts we do
|
|
15
15
|
// not have yet (install is a later phase). They are injected via config; week-1
|
|
@@ -132,8 +132,8 @@ function lowerRule(rule: IRPolicyRule, config: OzAdapterConfig): LoweredRule {
|
|
|
132
132
|
const policyRefs: PolicyRef[] = []
|
|
133
133
|
|
|
134
134
|
// scope -> context rule type. OZ scopes by contract (CallContract); a finer
|
|
135
|
-
// method-level restriction is a predicate concern and must be flagged
|
|
136
|
-
// because CallContract alone permits other methods on the same contract
|
|
135
|
+
// method-level restriction is a predicate concern and must be flagged as not
|
|
136
|
+
// covered because CallContract alone permits other methods on the same contract
|
|
137
137
|
// (e.g. an unbounded approve alongside a capped transfer).
|
|
138
138
|
const contextRuleType: ContextRuleDraft['contextRuleType'] =
|
|
139
139
|
rule.scope.contract !== undefined
|
|
@@ -171,12 +171,12 @@ function lowerRule(rule: IRPolicyRule, config: OzAdapterConfig): LoweredRule {
|
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
// guard ->
|
|
174
|
+
// guard -> not covered (applicability predicates are not an OZ built-in).
|
|
175
175
|
if (rule.guard) {
|
|
176
176
|
uncovered.push(`guard: ${describeCondition(rule.guard)}`)
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
// constraints -> spending_limit where they match; else
|
|
179
|
+
// constraints -> spending_limit where they match; else not covered. The OZ
|
|
180
180
|
// spending_limit policy takes `{ spending_limit: i128, period_ledgers: u32 }`
|
|
181
181
|
// and has NO token param: it only accepts a CallContract context rule
|
|
182
182
|
// (OnlyCallContractAllowed) and limits transfers of that context's contract,
|
|
@@ -209,7 +209,7 @@ function lowerRule(rule: IRPolicyRule, config: OzAdapterConfig): LoweredRule {
|
|
|
209
209
|
|
|
210
210
|
// approval.threshold -> simple/weighted threshold primitive. A threshold < 1
|
|
211
211
|
// is not a real M-of-N gate (0 approvals authorises everything), so refuse to
|
|
212
|
-
// emit a no-op primitive and flag it
|
|
212
|
+
// emit a no-op primitive and flag it as not covered instead.
|
|
213
213
|
if (rule.approval) {
|
|
214
214
|
if (!Number.isInteger(rule.approval.threshold) || rule.approval.threshold < 1) {
|
|
215
215
|
uncovered.push(
|
|
@@ -281,17 +281,17 @@ function matchSpendingLimit(c: IRCondition): SpendingLimitMatch | null {
|
|
|
281
281
|
return { token: selector.token, limit: value, windowSeconds: selector.windowSeconds }
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
-
/** Human-readable descriptor for a construct the OZ
|
|
285
|
-
* express, used to populate `uncovered
|
|
284
|
+
/** Human-readable descriptor for a construct the OZ built-in-primitive backend
|
|
285
|
+
* cannot express, used to populate `uncovered`. */
|
|
286
286
|
function describeCondition(cond: IRCondition): string {
|
|
287
287
|
switch (cond.op) {
|
|
288
288
|
case 'in':
|
|
289
|
-
return `value allowlist on ${describeSelector(cond.selector)} (arg allowlist
|
|
289
|
+
return `value allowlist on ${describeSelector(cond.selector)} (arg allowlist)`
|
|
290
290
|
case 'not':
|
|
291
|
-
return 'negated condition (predicate DSL
|
|
291
|
+
return 'negated condition (predicate DSL)'
|
|
292
292
|
case 'and':
|
|
293
293
|
case 'or':
|
|
294
|
-
return `nested ${cond.op} condition (predicate DSL
|
|
294
|
+
return `nested ${cond.op} condition (predicate DSL)`
|
|
295
295
|
case 'compare': {
|
|
296
296
|
const s = cond.compare.selector
|
|
297
297
|
switch (s.kind) {
|
|
@@ -302,16 +302,16 @@ function describeCondition(cond: IRCondition): string {
|
|
|
302
302
|
case 'window_spent':
|
|
303
303
|
return `spend-window comparison with operator '${cond.compare.operator}' (only 'lte' lowers to spending_limit)`
|
|
304
304
|
case 'amount':
|
|
305
|
-
return `per-call amount comparison on ${s.token} (predicate DSL
|
|
305
|
+
return `per-call amount comparison on ${s.token} (predicate DSL)`
|
|
306
306
|
case 'arg':
|
|
307
|
-
return `argument comparison on arg ${s.argIndex} (predicate DSL
|
|
307
|
+
return `argument comparison on arg ${s.argIndex} (predicate DSL)`
|
|
308
308
|
case 'calldata':
|
|
309
|
-
return 'EVM calldata comparison (predicate DSL
|
|
309
|
+
return 'EVM calldata comparison (predicate DSL)'
|
|
310
310
|
case 'value':
|
|
311
|
-
return 'tx.value comparison (predicate DSL
|
|
311
|
+
return 'tx.value comparison (predicate DSL)'
|
|
312
312
|
case 'now':
|
|
313
313
|
case 'valid_until':
|
|
314
|
-
return 'time comparison (predicate DSL
|
|
314
|
+
return 'time comparison (predicate DSL)'
|
|
315
315
|
}
|
|
316
316
|
}
|
|
317
317
|
}
|
package/src/mandate/to-ir.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
// contract/method -> rule.scope
|
|
7
7
|
// spendingLimit -> a `window_spent(token,window) <= limit` compare
|
|
8
8
|
// approvalThreshold-> rule.approval.threshold
|
|
9
|
-
// recipients -> an `in` condition on the recipient arg (
|
|
10
|
-
// by the OZ adapter; that is expected)
|
|
9
|
+
// recipients -> an `in` condition on the recipient arg (flagged as not
|
|
10
|
+
// covered by the OZ adapter; that is expected)
|
|
11
11
|
// expiry -> rule.expiry
|
|
12
12
|
// The top-level default is `deny_all` (OZ context rules are deny-by-default).
|
|
13
13
|
|
|
@@ -16,8 +16,8 @@ import type { MandateSpec } from './types.ts'
|
|
|
16
16
|
|
|
17
17
|
/** Arg index the recipient allowlist constrains. Pinned to the SEP-41
|
|
18
18
|
* `transfer(from, to, amount)` convention where `to` is arg 1. This condition
|
|
19
|
-
* is flagged
|
|
20
|
-
* allowlist), so the index only needs to be deterministic in week-1. */
|
|
19
|
+
* is flagged as not covered by the OZ adapter (no built-in primitive expresses
|
|
20
|
+
* an arg allowlist), so the index only needs to be deterministic in week-1. */
|
|
21
21
|
const RECIPIENT_ARG_INDEX = 1
|
|
22
22
|
|
|
23
23
|
export function mandateToPolicyIR(spec: MandateSpec): PolicyIR {
|
package/src/mandate/types.ts
CHANGED
|
@@ -14,7 +14,7 @@ export interface MandateSpec {
|
|
|
14
14
|
spendingLimit?: { token: string; limit: string; windowSeconds: number }
|
|
15
15
|
/** -> OZ `simple_threshold` / `weighted_threshold` primitive. */
|
|
16
16
|
approvalThreshold?: number
|
|
17
|
-
/** Recipient allowlist -> an `in` arg condition (
|
|
17
|
+
/** Recipient allowlist -> an `in` arg condition (not covered by an OZ built-in). */
|
|
18
18
|
recipients?: string[]
|
|
19
19
|
/** -> OZ context-rule expiry. */
|
|
20
20
|
expiry?: { validUntilLedger?: number; validUntilUnixSeconds?: number }
|
package/src/seams/types.ts
CHANGED
|
@@ -48,7 +48,7 @@ export interface CustodyCapabilities {
|
|
|
48
48
|
export interface CompileResult {
|
|
49
49
|
/** false => some IR construct this backend cannot express (see `uncovered`). */
|
|
50
50
|
covered: boolean
|
|
51
|
-
/** Human-readable list of unsupported constructs
|
|
51
|
+
/** Human-readable list of unsupported constructs. */
|
|
52
52
|
uncovered: string[]
|
|
53
53
|
/** The backend-native installable policy, assembled when a rule lowered. */
|
|
54
54
|
proposed?: ProposedPolicy
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
// src/synth/compose-from-recording.ts - facts + scope -> PolicyIR
|
|
1
|
+
// src/synth/compose-from-recording.ts - facts + scope -> PolicyIR.
|
|
2
2
|
//
|
|
3
|
-
// Composes the canonical IR rule the OZ
|
|
3
|
+
// Composes the canonical IR rule the OZ built-in-primitive backend can compile. Fail-closed
|
|
4
4
|
// composition rules:
|
|
5
5
|
//
|
|
6
6
|
// - identify the protocol of the top-level call (registry.identifyProtocol).
|
|
7
7
|
// When it is unknown (null), emit NO OZ-primitive-producing IR node: the
|
|
8
8
|
// scope is kept (CallContract + method) and every inferred bound is surfaced
|
|
9
|
-
// as a descriptive
|
|
9
|
+
// as a descriptive warning. An unrecognised call never compiles to a
|
|
10
10
|
// permissive OZ primitive.
|
|
11
11
|
//
|
|
12
12
|
// - carry the recorded top-level function into `rule.scope.method` so the OZ
|
|
13
|
-
// adapter flags per-method scoping
|
|
14
|
-
// on the contract; a per-method restriction needs the interpreter predicate).
|
|
13
|
+
// adapter flags per-method scoping as not covered (CallContract permits every
|
|
14
|
+
// method on the contract; a per-method restriction needs the interpreter predicate).
|
|
15
15
|
//
|
|
16
16
|
// - a `spending_limit` (window_spent(token, w) <= limit) is emitted ONLY when
|
|
17
17
|
// the caller supplies BOTH the limit (userResponses.limitAmount) and the
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
// - the IR carries ONLY constraints justified by the recording (observed
|
|
31
31
|
// recipient allowlist) + explicit user input. Nothing invented: no oracle
|
|
32
32
|
// price fabricated from a slippage bound, no synthetic exact-path compare.
|
|
33
|
-
// Those needs are surfaced as descriptive
|
|
33
|
+
// Those needs are surfaced as descriptive warnings instead.
|
|
34
34
|
//
|
|
35
35
|
// The default behavior is `deny_all` (OZ context rules are deny-by-default).
|
|
36
36
|
|
|
@@ -62,7 +62,7 @@ export interface ComposeOptions {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/** Result of composition: the PolicyIR, any ambiguities surfaced during
|
|
65
|
-
* inference, and descriptive
|
|
65
|
+
* inference, and descriptive warnings for needs that are NOT expressed
|
|
66
66
|
* as an IR node (so no fabricated constraint is emitted). The orchestrator
|
|
67
67
|
* carries ambiguities into `ProposedPolicy.ambiguities` and merges warnings
|
|
68
68
|
* into `ProposedPolicy.warnings`. */
|
|
@@ -104,7 +104,7 @@ export function composeFromRecording(
|
|
|
104
104
|
|
|
105
105
|
if (!known) {
|
|
106
106
|
warnings.push(
|
|
107
|
-
`spend of ${observed} (token ${token}) not bounded: unrecognised protocol, spend cap needs the interpreter predicate
|
|
107
|
+
`spend of ${observed} (token ${token}) not bounded: unrecognised protocol, spend cap needs the interpreter predicate`
|
|
108
108
|
)
|
|
109
109
|
continue
|
|
110
110
|
}
|
|
@@ -156,13 +156,13 @@ export function composeFromRecording(
|
|
|
156
156
|
question: 'Incoming-only flow - what max invocations per window should the policy enforce?',
|
|
157
157
|
})
|
|
158
158
|
warnings.push(
|
|
159
|
-
'frequency bound needed for the incoming-only flow (interpreter
|
|
159
|
+
'frequency bound needed for the incoming-only flow (needs the interpreter predicate); no invocation cap inferred'
|
|
160
160
|
)
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
// Observed recipient allowlist (SEP-41) is a real, recorded constraint the OZ
|
|
165
|
-
// adapter flags
|
|
165
|
+
// adapter flags as not covered. Unknown protocols emit nothing here.
|
|
166
166
|
if (topLevel && protocol !== null) {
|
|
167
167
|
appendProtocolSpecificConstraints(constraints, warnings, facts, topLevel, protocol)
|
|
168
168
|
}
|
|
@@ -189,7 +189,7 @@ export function composeFromRecording(
|
|
|
189
189
|
|
|
190
190
|
/** Add the constraints justified by the recording that the OZ backend cannot
|
|
191
191
|
* express natively (the SEP-41 recipient the transfer moved to). The OZ adapter
|
|
192
|
-
* flags them
|
|
192
|
+
* flags them as not covered. SoroSwap's slippage / oracle / exact-path needs are
|
|
193
193
|
* surfaced as descriptive warnings, NOT as fabricated comparison nodes. */
|
|
194
194
|
function appendProtocolSpecificConstraints(
|
|
195
195
|
constraints: IRCondition[],
|
|
@@ -199,7 +199,7 @@ function appendProtocolSpecificConstraints(
|
|
|
199
199
|
protocol: IdentifiedProtocol
|
|
200
200
|
): void {
|
|
201
201
|
// SEP-41 transfer / mint: the `to` arg (index 1) is the recipient. Emit it as
|
|
202
|
-
// a single-element allowlist; the OZ adapter flags it
|
|
202
|
+
// a single-element allowlist; the OZ adapter flags it as not covered.
|
|
203
203
|
if (protocol.protocol === 'sep41' && (protocol.fn === 'transfer' || protocol.fn === 'mint')) {
|
|
204
204
|
const toArg = topLevel.args[1]
|
|
205
205
|
if (toArg && toArg.type === 'address') {
|
|
@@ -220,7 +220,7 @@ function appendProtocolSpecificConstraints(
|
|
|
220
220
|
const route = paths?.length === 1 ? paths[0] : undefined
|
|
221
221
|
const pathText = route && route.length > 0 ? `; observed path: ${route.join(' -> ')}` : ''
|
|
222
222
|
warnings.push(
|
|
223
|
-
`SoroSwap swap: slippage / oracle price bound and exact hop path need the interpreter predicate
|
|
223
|
+
`SoroSwap swap: slippage / oracle price bound and exact hop path need the interpreter predicate${pathText}`
|
|
224
224
|
)
|
|
225
225
|
}
|
|
226
226
|
}
|
package/src/synth/lower.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface IntentFacts {
|
|
|
16
16
|
callTargets: string[]
|
|
17
17
|
functionsByContract: Record<string, string[]>
|
|
18
18
|
/** Aggregate outgoing spend per token (i128-safe decimal string). A single
|
|
19
|
-
* token entry is the
|
|
19
|
+
* token entry is the prerequisite for `spending_limit`. Movements
|
|
20
20
|
* with from != sourceAccount are NOT counted as "spend" by this synth
|
|
21
21
|
* (incoming yield, refund, etc.). */
|
|
22
22
|
spendByToken: Record<string, string>
|
|
@@ -27,7 +27,7 @@ export interface IntentFacts {
|
|
|
27
27
|
sharedRouter?: string
|
|
28
28
|
/** Router hop paths, keyed by target contract. SoroSwap exposes its `path`
|
|
29
29
|
* arg directly on the top-level call; this is the input the compose step
|
|
30
|
-
* passes to a per-arg `in` condition (flagged
|
|
30
|
+
* passes to a per-arg `in` condition (flagged as not covered by the OZ adapter). */
|
|
31
31
|
allowedPaths?: Record<string, string[][]>
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
// MandateSpec is lowered deterministically to a PolicyIR and compiled by the OZ
|
|
5
5
|
// adapter to a ProposedPolicy. No decoding, no inference, so parseConfidence is
|
|
6
6
|
// the full/not-applicable value. Constructs the OZ built-in primitives cannot
|
|
7
|
-
// express (compile's `uncovered`) are surfaced in `ProposedPolicy.warnings`
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// express (compile's `uncovered`) are surfaced in `ProposedPolicy.warnings`
|
|
8
|
+
// rather than failing the call - the covered primitives still install; the
|
|
9
|
+
// uncovered ones are reported.
|
|
10
10
|
|
|
11
11
|
import type { OzAdapterConfig } from '../adapters/oz/adapter.ts'
|
|
12
12
|
import { createOzAdapter } from '../adapters/oz/adapter.ts'
|
|
@@ -15,7 +15,7 @@ import { mandateToPolicyIR } from '../mandate/to-ir.ts'
|
|
|
15
15
|
import type { MandateSpec } from '../mandate/types.ts'
|
|
16
16
|
import type { ProposedPolicy } from '../types.ts'
|
|
17
17
|
|
|
18
|
-
const
|
|
18
|
+
const UNCOVERED_PREFIX = 'Not covered by OZ built-in primitives: '
|
|
19
19
|
|
|
20
20
|
export function synthesizeFromMandate(
|
|
21
21
|
spec: MandateSpec,
|
|
@@ -40,7 +40,7 @@ export function synthesizeFromMandate(
|
|
|
40
40
|
|
|
41
41
|
const proposed: ProposedPolicy = {
|
|
42
42
|
...result.proposed,
|
|
43
|
-
warnings: result.uncovered.map((u) => `${
|
|
43
|
+
warnings: result.uncovered.map((u) => `${UNCOVERED_PREFIX}${u}`),
|
|
44
44
|
}
|
|
45
45
|
return { ok: true, data: proposed }
|
|
46
46
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// 4. `composeFromRecording(facts, scope, opts)` -> PolicyIR + ambiguities.
|
|
12
12
|
// 5. `ozAdapter.compile(ir)` -> CompileResult.
|
|
13
13
|
// 6. Assemble ProposedPolicy carrying parseConfidence (from tx), warnings
|
|
14
|
-
// (the OZ `uncovered`
|
|
14
|
+
// (the OZ `uncovered` markers, prefixed), and ambiguities
|
|
15
15
|
// (DURATION_UNSPECIFIED etc.).
|
|
16
16
|
//
|
|
17
17
|
// Determinism: same (tx, opts, ozConfig) -> byte-identical ProposedPolicy.
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
import { lower } from './lower.ts'
|
|
35
35
|
import { decideScope, type ScopeDecision } from './scope.ts'
|
|
36
36
|
|
|
37
|
-
const
|
|
37
|
+
const UNCOVERED_PREFIX = 'Not covered by OZ built-in primitives: '
|
|
38
38
|
|
|
39
39
|
/** Top-level orchestrator inputs. `userResponses` carries the LLM-collected
|
|
40
40
|
* answers to the ambiguity prompts (windowSeconds, validUntilLedger,
|
|
@@ -98,7 +98,7 @@ export function synthesizeFromRecording(
|
|
|
98
98
|
if (!scopeRes.ok) return scopeRes
|
|
99
99
|
const scope: ScopeDecision = scopeRes.data
|
|
100
100
|
if (scope.kind !== 'call_contract') {
|
|
101
|
-
// A default scope on the recording-path is not currently a
|
|
101
|
+
// A default scope on the recording-path is not currently a covered flow;
|
|
102
102
|
// surface SYNTHESIS_ERROR so the orchestrator never silently emits an
|
|
103
103
|
// over-broad default-scoped policy.
|
|
104
104
|
return {
|
|
@@ -142,8 +142,8 @@ export function synthesizeFromRecording(
|
|
|
142
142
|
...compileRes.proposed,
|
|
143
143
|
parseConfidence: { ...tx.parseConfidence },
|
|
144
144
|
warnings: [
|
|
145
|
-
...compileRes.uncovered.map((u) => `${
|
|
146
|
-
...composed.warnings.map((w) => `${
|
|
145
|
+
...compileRes.uncovered.map((u) => `${UNCOVERED_PREFIX}${u}`),
|
|
146
|
+
...composed.warnings.map((w) => `${UNCOVERED_PREFIX}${w}`),
|
|
147
147
|
],
|
|
148
148
|
ambiguities: mergeAmbiguities(composed.ambiguities, scope.ambiguities),
|
|
149
149
|
}
|