@crediolabs/policy-synth 0.2.0 → 0.3.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.
- package/README.md +4 -4
- package/dist/adapters/interpreter/adapter.d.ts +19 -25
- package/dist/adapters/interpreter/adapter.js +54 -487
- package/dist/adapters/interpreter/index.d.ts +1 -1
- package/dist/adapters/interpreter/index.js +1 -1
- package/dist/adapters/oz/adapter.js +1 -15
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +1 -4
- package/dist/index.js +1 -4
- package/dist/install/build-add-context-rule.d.ts +3 -19
- package/dist/install/build-add-context-rule.js +3 -98
- package/dist/install/build-install-policy.d.ts +4 -53
- package/dist/install/build-install-policy.js +44 -123
- package/dist/install/index.d.ts +0 -2
- package/dist/install/index.js +0 -7
- package/dist/ir/index.d.ts +1 -1
- package/dist/ir/types.d.ts +7 -76
- package/dist/ir/types.js +0 -2
- package/dist/predicate/decode.d.ts +1 -1
- package/dist/predicate/decode.js +2 -70
- package/dist/predicate/encode.js +45 -229
- package/dist/predicate/from-json.js +1 -42
- package/dist/record/decode.js +3 -8
- package/dist/review-card/builder.d.ts +4 -3
- package/dist/review-card/builder.js +18 -162
- package/dist/review-card/cross-check.js +2 -105
- package/dist/review-card/render-leaf.d.ts +4 -0
- package/dist/review-card/render-leaf.js +47 -0
- package/dist/run/index.d.ts +36 -139
- package/dist/run/index.js +125 -437
- package/dist/run/schemas.d.ts +168 -1929
- package/dist/run/schemas.js +40 -269
- package/dist/seams/types.d.ts +0 -2
- package/dist/simulate/deny-cases.d.ts +26 -0
- package/dist/simulate/deny-cases.js +282 -0
- package/dist/simulate/evaluate.d.ts +18 -0
- package/dist/simulate/evaluate.js +271 -0
- package/dist/simulate/index.d.ts +4 -0
- package/dist/simulate/index.js +8 -0
- package/dist/synth/compose-from-recording.d.ts +24 -51
- package/dist/synth/compose-from-recording.js +120 -282
- package/dist/synth/deny-cases.d.ts +5 -7
- package/dist/synth/deny-cases.js +23 -373
- package/dist/synth/evaluate.d.ts +0 -21
- package/dist/synth/evaluate.js +56 -336
- package/dist/synth/index.d.ts +0 -5
- package/dist/synth/index.js +0 -5
- package/dist/synth/permit-context.d.ts +2 -9
- package/dist/synth/permit-context.js +1 -82
- package/dist/synth/synthesize-from-recording.d.ts +1 -29
- package/dist/synth/synthesize-from-recording.js +41 -356
- package/dist/types.d.ts +8 -60
- package/dist/types.js +5 -2
- package/dist/verify/simulate.d.ts +1 -14
- package/dist/verify/simulate.js +2 -88
- package/dist/verify/verify.d.ts +1 -7
- package/dist/verify/verify.js +0 -8
- package/dist-cjs/adapters/interpreter/adapter.d.ts +19 -25
- package/dist-cjs/adapters/interpreter/adapter.js +56 -489
- package/dist-cjs/adapters/interpreter/index.d.ts +1 -1
- package/dist-cjs/adapters/interpreter/index.js +1 -2
- package/dist-cjs/adapters/oz/adapter.js +1 -15
- package/dist-cjs/errors.d.ts +1 -1
- package/dist-cjs/index.d.ts +1 -4
- package/dist-cjs/index.js +1 -4
- package/dist-cjs/install/build-add-context-rule.d.ts +3 -19
- package/dist-cjs/install/build-add-context-rule.js +1 -97
- package/dist-cjs/install/build-install-policy.d.ts +4 -53
- package/dist-cjs/install/build-install-policy.js +44 -122
- package/dist-cjs/install/index.d.ts +0 -2
- package/dist-cjs/install/index.js +2 -23
- package/dist-cjs/ir/index.d.ts +1 -1
- package/dist-cjs/ir/types.d.ts +7 -76
- package/dist-cjs/ir/types.js +0 -2
- package/dist-cjs/predicate/decode.d.ts +1 -1
- package/dist-cjs/predicate/decode.js +2 -70
- package/dist-cjs/predicate/encode.js +45 -229
- package/dist-cjs/predicate/from-json.js +1 -42
- package/dist-cjs/record/decode.js +3 -8
- package/dist-cjs/review-card/builder.d.ts +4 -3
- package/dist-cjs/review-card/builder.js +23 -167
- package/dist-cjs/review-card/cross-check.js +7 -110
- package/dist-cjs/review-card/render-leaf.d.ts +4 -0
- package/dist-cjs/review-card/render-leaf.js +52 -0
- package/dist-cjs/run/index.d.ts +36 -139
- package/dist-cjs/run/index.js +125 -444
- package/dist-cjs/run/schemas.d.ts +168 -1929
- package/dist-cjs/run/schemas.js +41 -270
- package/dist-cjs/seams/types.d.ts +0 -2
- package/dist-cjs/simulate/deny-cases.d.ts +26 -0
- package/dist-cjs/simulate/deny-cases.js +286 -0
- package/dist-cjs/simulate/evaluate.d.ts +18 -0
- package/dist-cjs/simulate/evaluate.js +274 -0
- package/dist-cjs/simulate/index.d.ts +4 -0
- package/dist-cjs/simulate/index.js +13 -0
- package/dist-cjs/synth/compose-from-recording.d.ts +24 -51
- package/dist-cjs/synth/compose-from-recording.js +120 -282
- package/dist-cjs/synth/deny-cases.d.ts +5 -7
- package/dist-cjs/synth/deny-cases.js +23 -374
- package/dist-cjs/synth/evaluate.d.ts +0 -21
- package/dist-cjs/synth/evaluate.js +57 -337
- package/dist-cjs/synth/index.d.ts +0 -5
- package/dist-cjs/synth/index.js +1 -11
- package/dist-cjs/synth/permit-context.d.ts +2 -9
- package/dist-cjs/synth/permit-context.js +1 -82
- package/dist-cjs/synth/synthesize-from-recording.d.ts +1 -29
- package/dist-cjs/synth/synthesize-from-recording.js +39 -354
- package/dist-cjs/types.d.ts +8 -60
- package/dist-cjs/types.js +6 -3
- package/dist-cjs/verify/simulate.d.ts +1 -14
- package/dist-cjs/verify/simulate.js +2 -88
- package/dist-cjs/verify/verify.d.ts +1 -7
- package/dist-cjs/verify/verify.js +0 -8
- package/package.json +4 -4
- package/src/adapters/interpreter/adapter.ts +76 -572
- package/src/errors.ts +0 -19
- package/src/index.ts +1 -4
- package/src/install/build-add-context-rule.ts +5 -139
- package/src/install/build-install-policy.ts +93 -214
- package/src/install/index.ts +0 -34
- package/src/predicate/decode.ts +2 -70
- package/src/predicate/encode.ts +49 -261
- package/src/predicate/from-json.ts +1 -43
- package/src/record/decode.ts +3 -8
- package/src/review-card/builder.ts +19 -168
- package/src/review-card/cross-check.ts +3 -105
- package/src/review-card/render-leaf.ts +48 -0
- package/src/run/index.ts +144 -572
- package/src/run/schemas.ts +42 -291
- package/src/simulate/deny-cases.ts +334 -0
- package/src/simulate/evaluate.ts +284 -0
- package/src/simulate/index.ts +11 -0
- package/src/synth/compose-from-recording.ts +148 -347
- package/src/synth/index.ts +0 -13
- package/src/synth/synthesize-from-recording.ts +43 -456
- package/src/types.ts +13 -49
- package/dist/install/authority-overlap.d.ts +0 -134
- package/dist/install/authority-overlap.js +0 -0
- package/dist/install/build-install-predicate.d.ts +0 -96
- package/dist/install/build-install-predicate.js +0 -444
- package/dist/install/build-merge-policy.d.ts +0 -70
- package/dist/install/build-merge-policy.js +0 -130
- package/dist/install/plan-merge-policy.d.ts +0 -49
- package/dist/install/plan-merge-policy.js +0 -86
- package/dist/install/read-account-rules.d.ts +0 -100
- package/dist/install/read-account-rules.js +0 -283
- package/dist-cjs/install/authority-overlap.d.ts +0 -134
- package/dist-cjs/install/authority-overlap.js +0 -0
- package/dist-cjs/install/build-install-predicate.d.ts +0 -96
- package/dist-cjs/install/build-install-predicate.js +0 -479
- package/dist-cjs/install/build-merge-policy.d.ts +0 -70
- package/dist-cjs/install/build-merge-policy.js +0 -134
- package/dist-cjs/install/plan-merge-policy.d.ts +0 -49
- package/dist-cjs/install/plan-merge-policy.js +0 -90
- package/dist-cjs/install/read-account-rules.d.ts +0 -100
- package/dist-cjs/install/read-account-rules.js +0 -296
- package/src/adapters/interpreter/index.ts +0 -8
- package/src/adapters/oz/adapter.ts +0 -376
- package/src/adapters/oz/index.ts +0 -9
- package/src/codegen/compile-gate.ts +0 -167
- package/src/codegen/index.ts +0 -17
- package/src/codegen/template.ts +0 -165
- package/src/install/authority-overlap.ts +0 -0
- package/src/install/build-merge-policy.ts +0 -219
- package/src/install/plan-merge-policy.ts +0 -133
- package/src/install/read-account-rules.ts +0 -376
- package/src/ir/index.ts +0 -13
- package/src/ir/types.ts +0 -132
- package/src/mandate/index.ts +0 -4
- package/src/mandate/to-ir.ts +0 -71
- package/src/mandate/types.ts +0 -21
- package/src/seams/index.ts +0 -11
- package/src/seams/types.ts +0 -81
- package/src/synth/deny-cases.ts +0 -663
- package/src/synth/evaluate.ts +0 -613
- package/src/synth/harness.ts +0 -68
- package/src/synth/minimize.ts +0 -48
- package/src/synth/permit-context.ts +0 -136
- package/src/synth/predicate-literals.ts +0 -27
- package/src/synth/synthesize-from-mandate.ts +0 -82
- package/src/verify/envelope.ts +0 -28
- package/src/verify/index.ts +0 -5
- package/src/verify/simulate.ts +0 -311
- package/src/verify/verify.ts +0 -243
|
@@ -2,33 +2,28 @@
|
|
|
2
2
|
// src/synth/synthesize-from-recording.ts - recording-path orchestrator.
|
|
3
3
|
//
|
|
4
4
|
// `synthesizeFromRecording` INFERS a bounded policy from a `RecordedTransaction`
|
|
5
|
-
// via the
|
|
5
|
+
// via the `PolicyIR` + interpreter adapter pair.
|
|
6
6
|
// Flow: validate -> parseConfidence gate -> lower -> decideScope -> composeFromRecording
|
|
7
|
-
// ->
|
|
8
|
-
// Same (tx, opts
|
|
7
|
+
// -> interpreter compile + self-verify.
|
|
8
|
+
// Same (tx, opts) -> byte-identical ProposedPolicy (no randomness, clock, globals).
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
exports.synthesizeFromRecording = synthesizeFromRecording;
|
|
11
11
|
exports.throwToolError = throwToolError;
|
|
12
12
|
const adapter_ts_1 = require("../adapters/interpreter/adapter.js");
|
|
13
|
-
const adapter_ts_2 = require("../adapters/oz/adapter.js");
|
|
14
13
|
const encode_ts_1 = require("../predicate/encode.js");
|
|
15
14
|
const types_ts_1 = require("../types.js");
|
|
16
15
|
const compose_from_recording_ts_1 = require("./compose-from-recording.js");
|
|
17
|
-
const deny_cases_ts_1 = require("./deny-cases.js");
|
|
18
|
-
const evaluate_ts_1 = require("./evaluate.js");
|
|
19
|
-
const harness_ts_1 = require("./harness.js");
|
|
20
16
|
const lower_ts_1 = require("./lower.js");
|
|
21
|
-
const minimize_ts_1 = require("./minimize.js");
|
|
22
17
|
const scope_ts_1 = require("./scope.js");
|
|
23
|
-
const UNCOVERED_PREFIX = 'Not
|
|
24
|
-
function synthesizeFromRecording(tx, opts
|
|
18
|
+
const UNCOVERED_PREFIX = 'Not expressible as a predicate constraint: ';
|
|
19
|
+
function synthesizeFromRecording(tx, opts) {
|
|
25
20
|
// Convert any ToolError-shaped throw (object with a string `.code`) to a
|
|
26
21
|
// structured `{ok:false, error}`; anything else is rethrown so genuine bugs
|
|
27
22
|
// crash instead of being silently swallowed. Wraps the entire body so
|
|
28
|
-
// cap errors (PREDICATE_TOO_DEEP, TOO_MANY_LEAVES) and
|
|
23
|
+
// cap errors (PREDICATE_TOO_DEEP, TOO_MANY_LEAVES) and encoder depth
|
|
29
24
|
// throws surface as structured ToolErrors rather than RangeErrors.
|
|
30
25
|
try {
|
|
31
|
-
return synthesizeFromRecordingInner(tx, opts
|
|
26
|
+
return synthesizeFromRecordingInner(tx, opts);
|
|
32
27
|
}
|
|
33
28
|
catch (e) {
|
|
34
29
|
if (isToolErrorShape(e)) {
|
|
@@ -64,7 +59,7 @@ function throwToolError(code, message) {
|
|
|
64
59
|
err.retryable = false;
|
|
65
60
|
throw err;
|
|
66
61
|
}
|
|
67
|
-
function synthesizeFromRecordingInner(tx, opts
|
|
62
|
+
function synthesizeFromRecordingInner(tx, opts) {
|
|
68
63
|
// 0. validate inputs (fail closed - never synthesize from garbage).
|
|
69
64
|
const invalid = validateOptions(opts);
|
|
70
65
|
if (invalid)
|
|
@@ -156,7 +151,6 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
156
151
|
const topLevel = tx.invocations[0] ?? null;
|
|
157
152
|
const composeOpts = {
|
|
158
153
|
network: opts.network,
|
|
159
|
-
interpreterEnabled: opts.interpreter !== undefined,
|
|
160
154
|
...(opts.userResponses !== undefined ? { userResponses: opts.userResponses } : {}),
|
|
161
155
|
};
|
|
162
156
|
const composed = (0, compose_from_recording_ts_1.composeFromRecording)(facts, scope.contract, topLevel, composeOpts);
|
|
@@ -164,37 +158,17 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
164
158
|
// self-verify verdict (built from the SAME runHarness + evaluate that gated
|
|
165
159
|
// the synthesis, not a parallel simulation).
|
|
166
160
|
let explain = null;
|
|
167
|
-
let explainSim = null;
|
|
168
|
-
// 5. OZ compile (always runs).
|
|
169
|
-
const ozAdapter = (0, adapter_ts_2.createOzAdapter)(ozConfig);
|
|
170
|
-
const compileRes = ozAdapter.compile(composed.ir);
|
|
171
|
-
if (!compileRes.proposed) {
|
|
172
|
-
return {
|
|
173
|
-
ok: false,
|
|
174
|
-
error: {
|
|
175
|
-
code: 'SYNTHESIS_ERROR',
|
|
176
|
-
message: `recording lowered to no installable OZ policy: ${compileRes.uncovered.join('; ')}`,
|
|
177
|
-
severity: 'error',
|
|
178
|
-
retryable: false,
|
|
179
|
-
details: { uncovered: compileRes.uncovered },
|
|
180
|
-
},
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
161
|
// 6. Interpreter compile (opt-in; fail-closed when the user routed real
|
|
184
162
|
// restrictions to the interpreter).
|
|
185
163
|
const interpreterOpts = opts.interpreter;
|
|
186
164
|
let interpreterPolicyDocument = null;
|
|
187
165
|
let interpreterPolicyRef = null;
|
|
188
|
-
|
|
189
|
-
// warnings folded into `proposed.warnings[]` (which lives after that block)
|
|
190
|
-
// can read it. The block assigns it; the default is empty.
|
|
191
|
-
let permitCtxWarnings = [];
|
|
166
|
+
let interpreterContextRule = null;
|
|
192
167
|
if (interpreterOpts) {
|
|
193
168
|
const interpreterConfig = {
|
|
194
169
|
network: opts.network,
|
|
195
170
|
installNonce: interpreterOpts.installNonce ?? 1,
|
|
196
171
|
smartAccountAddress: interpreterOpts.smartAccountAddress,
|
|
197
|
-
...(interpreterOpts.oracleParams ? { oracleParams: interpreterOpts.oracleParams } : {}),
|
|
198
172
|
};
|
|
199
173
|
let startingPredicate = null;
|
|
200
174
|
// `__testPredicateNode` is a test-only seam. It is NOT in the public
|
|
@@ -211,23 +185,19 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
211
185
|
startingPredicate = testSeam;
|
|
212
186
|
}
|
|
213
187
|
else {
|
|
214
|
-
const interpreterAdapter = (0, adapter_ts_1.createInterpreterAdapter)(interpreterConfig);
|
|
215
188
|
let interpreterRes;
|
|
216
189
|
try {
|
|
217
|
-
interpreterRes =
|
|
190
|
+
interpreterRes = (0, adapter_ts_1.compileInterpreterPolicy)(composed.interpreterRule, interpreterConfig);
|
|
218
191
|
}
|
|
219
192
|
catch (e) {
|
|
220
193
|
const code = e.code;
|
|
221
194
|
const allowedCodes = [
|
|
222
195
|
'SCOPE_SELF_CALL',
|
|
223
|
-
'ORACLE_LEAF_INVALID_POSITION',
|
|
224
|
-
'ORACLE_PARAMS_OUT_OF_RANGE',
|
|
225
196
|
'MALFORMED_PREDICATE',
|
|
226
197
|
'PREDICATE_TOO_LARGE',
|
|
227
198
|
'PREDICATE_TOO_DEEP',
|
|
228
199
|
'TOO_MANY_LEAVES',
|
|
229
200
|
'IN_OPERAND_LIMIT',
|
|
230
|
-
'PREDICATE_ORACLE_OVER_LIMIT',
|
|
231
201
|
];
|
|
232
202
|
const surfacedCode = code && allowedCodes.includes(code) ? code : 'SYNTHESIS_ERROR';
|
|
233
203
|
return {
|
|
@@ -265,6 +235,7 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
265
235
|
}
|
|
266
236
|
interpreterPolicyDocument = interpreterRes.proposed.policyDocuments[0] ?? null;
|
|
267
237
|
interpreterPolicyRef = interpreterRes.proposed.policyRefs[0] ?? null;
|
|
238
|
+
interpreterContextRule = interpreterRes.proposed.contextRule;
|
|
268
239
|
if (!interpreterPolicyDocument || !interpreterPolicyRef) {
|
|
269
240
|
return {
|
|
270
241
|
ok: false,
|
|
@@ -276,7 +247,7 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
276
247
|
},
|
|
277
248
|
};
|
|
278
249
|
}
|
|
279
|
-
const firstInterpreterRule = composed.
|
|
250
|
+
const firstInterpreterRule = composed.interpreterRule;
|
|
280
251
|
if (!firstInterpreterRule) {
|
|
281
252
|
return {
|
|
282
253
|
ok: false,
|
|
@@ -301,7 +272,9 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
301
272
|
},
|
|
302
273
|
};
|
|
303
274
|
}
|
|
304
|
-
// 6b.
|
|
275
|
+
// 6b. The predicate is used as compiled. There is no minimisation pass:
|
|
276
|
+
// nothing strips a leaf and re-checks that the reduced predicate still
|
|
277
|
+
// denies, so the result is NOT a proven-minimal policy.
|
|
305
278
|
if (!topLevel) {
|
|
306
279
|
return {
|
|
307
280
|
ok: false,
|
|
@@ -324,81 +297,21 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
324
297
|
},
|
|
325
298
|
};
|
|
326
299
|
}
|
|
327
|
-
|
|
328
|
-
// only the oracle-on-right normaliser) and folded into the proposed
|
|
329
|
-
// policy's `warnings[]` so the caller sees them on the success envelope.
|
|
330
|
-
const permitCtxResult = buildPermitContext(tx, scope, topLevel, opts.userResponses, startingPredicate);
|
|
331
|
-
const permitCtx = permitCtxResult.ctx;
|
|
332
|
-
permitCtxWarnings = permitCtxResult.warnings;
|
|
333
|
-
const finalPredicate = startingPredicate.op === 'and'
|
|
334
|
-
? (0, minimize_ts_1.minimize)(startingPredicate, permitCtx, deny_cases_ts_1.ORIGINAL_DIMENSIONS)
|
|
335
|
-
: startingPredicate;
|
|
336
|
-
const harnessCases = (0, deny_cases_ts_1.generateCases)(finalPredicate, permitCtx, deny_cases_ts_1.ORIGINAL_DIMENSIONS);
|
|
337
|
-
const harnessResult = (0, harness_ts_1.runHarness)(finalPredicate, harnessCases);
|
|
338
|
-
if (!harnessResult.ok) {
|
|
339
|
-
return {
|
|
340
|
-
ok: false,
|
|
341
|
-
error: {
|
|
342
|
-
code: 'DENY_CASE_FAILURE',
|
|
343
|
-
message: `self-verify harness failed for the interpreter predicate (${harnessResult.failures.length} failure(s))`,
|
|
344
|
-
severity: 'error',
|
|
345
|
-
retryable: false,
|
|
346
|
-
details: { failures: harnessResult.failures },
|
|
347
|
-
},
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
const evalResult = (0, evaluate_ts_1.evaluate)(finalPredicate, permitCtx);
|
|
351
|
-
if (!evalResult.permit) {
|
|
352
|
-
return {
|
|
353
|
-
ok: false,
|
|
354
|
-
error: {
|
|
355
|
-
code: 'DENY_CASE_FAILURE',
|
|
356
|
-
message: `intended recorded call was denied by the interpreter predicate: ${evalResult.reason}`,
|
|
357
|
-
severity: 'error',
|
|
358
|
-
retryable: false,
|
|
359
|
-
details: { reason: evalResult.reason },
|
|
360
|
-
},
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
// --explain capture: quote the SAME verdict that gated the synthesis.
|
|
364
|
-
// Re-evaluate each deny case to surface its concrete reason (the harness
|
|
365
|
-
// only records whether the got-matches-expected boundary held).
|
|
300
|
+
const finalPredicate = startingPredicate;
|
|
366
301
|
if (opts.explain) {
|
|
367
302
|
explain = finalPredicate;
|
|
368
|
-
const evaluatedCases = [
|
|
369
|
-
{
|
|
370
|
-
dimension: 'permit',
|
|
371
|
-
outcome: evalResult.permit ? 'permit' : 'deny',
|
|
372
|
-
reason: 'matches recorded call',
|
|
373
|
-
},
|
|
374
|
-
];
|
|
375
|
-
for (const deny of harnessCases.denies) {
|
|
376
|
-
const r = (0, evaluate_ts_1.evaluate)(finalPredicate, deny.ctx);
|
|
377
|
-
evaluatedCases.push({
|
|
378
|
-
dimension: deny.dimension,
|
|
379
|
-
outcome: r.permit ? 'permit' : 'deny',
|
|
380
|
-
reason: r.permit ? 'no matching deny' : r.reason,
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
explainSim = {
|
|
384
|
-
permit: { tx: 'permit' },
|
|
385
|
-
evaluatedCases,
|
|
386
|
-
backend: 'ts-model',
|
|
387
|
-
simulatorVersion: 'ts-model-1.0.0',
|
|
388
|
-
};
|
|
389
303
|
}
|
|
390
|
-
// 6c.
|
|
391
|
-
//
|
|
304
|
+
// 6c. Encode the PredicateNode and stamp the canonical bytes back onto
|
|
305
|
+
// the PolicyDocument + PolicyRef. Cap breaches
|
|
392
306
|
// (PREDICATE_TOO_DEEP, TOO_MANY_LEAVES) throw ToolError-shaped errors;
|
|
393
307
|
// the outer envelope converts them to structured `{ok:false, error}`.
|
|
394
308
|
const { encodedPredicate, predicateHash } = (0, encode_ts_1.encodePredicate)(finalPredicate);
|
|
395
309
|
if (testSeam !== undefined) {
|
|
396
310
|
interpreterPolicyDocument = {
|
|
397
|
-
grammarVersion:
|
|
311
|
+
grammarVersion: types_ts_1.GRAMMAR_VERSION,
|
|
398
312
|
installNonce: interpreterOpts.installNonce ?? 1,
|
|
399
313
|
encodedPredicate,
|
|
400
314
|
predicateHash,
|
|
401
|
-
...(interpreterOpts.oracleParams ? { oracleParams: interpreterOpts.oracleParams } : {}),
|
|
402
315
|
};
|
|
403
316
|
interpreterPolicyRef = {
|
|
404
317
|
kind: 'interpreter',
|
|
@@ -431,20 +344,23 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
431
344
|
}
|
|
432
345
|
}
|
|
433
346
|
}
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
347
|
+
// 7. Assemble the ProposedPolicy. The interpreter is the only backend, so
|
|
348
|
+
// its context rule and policy ref are the whole policy. Without interpreter
|
|
349
|
+
// options nothing installable was produced, which is an error rather than
|
|
350
|
+
// an empty-but-successful policy.
|
|
351
|
+
if (!interpreterContextRule || !interpreterPolicyRef) {
|
|
352
|
+
return {
|
|
353
|
+
ok: false,
|
|
354
|
+
error: {
|
|
355
|
+
code: 'SYNTHESIS_ERROR',
|
|
356
|
+
message: 'no installable policy was synthesised: supply `interpreter` options (a smart account address) so the recording can be lowered to an interpreter predicate',
|
|
357
|
+
severity: 'error',
|
|
358
|
+
retryable: false,
|
|
359
|
+
details: { uncovered: composed.warnings },
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
const mergedRefs = [interpreterPolicyRef];
|
|
448
364
|
if (mergedRefs.length > types_ts_1.OZ_LIMITS.maxPoliciesPerRule) {
|
|
449
365
|
return {
|
|
450
366
|
ok: false,
|
|
@@ -457,7 +373,7 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
457
373
|
};
|
|
458
374
|
}
|
|
459
375
|
const mergedContextRule = {
|
|
460
|
-
...
|
|
376
|
+
...interpreterContextRule,
|
|
461
377
|
policies: mergedRefs,
|
|
462
378
|
};
|
|
463
379
|
// When nothing installable was synthesised (no interpreter doc AND no OZ
|
|
@@ -474,12 +390,7 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
474
390
|
policyDocuments: interpreterPolicyDocument ? [interpreterPolicyDocument] : [],
|
|
475
391
|
policyRefs: mergedRefs,
|
|
476
392
|
parseConfidence: { ...tx.parseConfidence },
|
|
477
|
-
warnings: [
|
|
478
|
-
...zeroPolicyWarning,
|
|
479
|
-
...ozUncovered.map((u) => `${UNCOVERED_PREFIX}${u}`),
|
|
480
|
-
...composed.warnings.map((w) => `${UNCOVERED_PREFIX}${w}`),
|
|
481
|
-
...permitCtxWarnings,
|
|
482
|
-
],
|
|
393
|
+
warnings: [...zeroPolicyWarning, ...composed.warnings.map((w) => `${UNCOVERED_PREFIX}${w}`)],
|
|
483
394
|
ambiguities: mergeAmbiguities(composed.ambiguities, scope.ambiguities),
|
|
484
395
|
};
|
|
485
396
|
// --explain success envelope. When opts.explain is set and the OZ-only
|
|
@@ -488,40 +399,10 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
488
399
|
// permit is deny with a truthful reason and evaluatedCases is empty.
|
|
489
400
|
const envelope = { ok: true, data: proposed };
|
|
490
401
|
if (opts.explain) {
|
|
491
|
-
|
|
492
|
-
envelope.explain = { predicateTree: explain, simulation: explainSim };
|
|
493
|
-
}
|
|
494
|
-
else {
|
|
495
|
-
envelope.explain = {
|
|
496
|
-
predicateTree: null,
|
|
497
|
-
simulation: {
|
|
498
|
-
permit: {
|
|
499
|
-
tx: 'deny',
|
|
500
|
-
reason: 'No self-verification was performed (interpreter adapter was not engaged)',
|
|
501
|
-
},
|
|
502
|
-
evaluatedCases: [],
|
|
503
|
-
backend: 'ts-model',
|
|
504
|
-
simulatorVersion: 'not-run',
|
|
505
|
-
},
|
|
506
|
-
};
|
|
507
|
-
}
|
|
402
|
+
envelope.explain = { predicateTree: explain };
|
|
508
403
|
}
|
|
509
404
|
return envelope;
|
|
510
405
|
}
|
|
511
|
-
/** OZ-side `uncovered` warning patterns the interpreter adapter actually lowers
|
|
512
|
-
* when wired in. When the interpreter adapter succeeds, matching entries are
|
|
513
|
-
* dropped from the OZ uncovered list so user-facing warnings reflect what is
|
|
514
|
-
* still UN-enforced rather than what OZ alone could not do. Matches the
|
|
515
|
-
* exact descriptor strings the OZ adapter emits (see `src/adapters/oz/adapter.ts`
|
|
516
|
-
* `describeCondition` / `describeSelector`). */
|
|
517
|
-
const INTERPRETER_COVERED_OZ_PATTERN = /^per-method scoping to|^value allowlist on arg|^exact ordered sequence on arg|^oracle price condition on|^invocation-count window|^spending_limit on token .+ needs a CallContract context scoped to that token/;
|
|
518
|
-
/** Reject non-sane inputs before any policy is synthesized. windowSeconds /
|
|
519
|
-
* validUntilLedger / invocationLimit must be positive integers; limitAmount a
|
|
520
|
-
* positive i128 decimal string; network mainnet|testnet. When the interpreter
|
|
521
|
-
* adapter is opted in, `smartAccountAddress` must be a C... contract address
|
|
522
|
-
* (the on-chain policy-bound account, NOT the G... source account),
|
|
523
|
-
* `installNonce` must fit u32 (default 1), and `oracleParams` must
|
|
524
|
-
* tighten-only vs the wasm defaults. */
|
|
525
406
|
function validateOptions(opts) {
|
|
526
407
|
if (opts.network !== 'mainnet' && opts.network !== 'testnet') {
|
|
527
408
|
return synthesisError(`network must be 'mainnet' or 'testnet', got: ${String(opts.network)}`);
|
|
@@ -534,16 +415,10 @@ function validateOptions(opts) {
|
|
|
534
415
|
}
|
|
535
416
|
const ur = opts.userResponses;
|
|
536
417
|
if (ur) {
|
|
537
|
-
if (ur.windowSeconds !== undefined && !isPositiveInt(ur.windowSeconds)) {
|
|
538
|
-
return synthesisError(`windowSeconds must be a positive integer, got: ${ur.windowSeconds}`);
|
|
539
|
-
}
|
|
540
418
|
if (ur.validUntilLedger !== undefined &&
|
|
541
419
|
(!isPositiveInt(ur.validUntilLedger) || ur.validUntilLedger > types_ts_1.SOROBAN_LIMITS.u32Max)) {
|
|
542
420
|
return synthesisError(`validUntilLedger must be a positive u32 ledger sequence (<= ${types_ts_1.SOROBAN_LIMITS.u32Max}), got: ${ur.validUntilLedger}`);
|
|
543
421
|
}
|
|
544
|
-
if (ur.invocationLimit !== undefined && !isPositiveInt(ur.invocationLimit)) {
|
|
545
|
-
return synthesisError(`invocationLimit must be a positive integer, got: ${ur.invocationLimit}`);
|
|
546
|
-
}
|
|
547
422
|
if (ur.limitAmount !== undefined && !isPositiveI128(ur.limitAmount)) {
|
|
548
423
|
return synthesisError(`limitAmount must be a positive i128 decimal string within [1, ${I128_MAX}] (2^127-1), got: ${ur.limitAmount}`);
|
|
549
424
|
}
|
|
@@ -567,19 +442,6 @@ function validateOptions(opts) {
|
|
|
567
442
|
if (nonce !== undefined && (!isPositiveInt(nonce) || nonce > types_ts_1.SOROBAN_LIMITS.u32Max)) {
|
|
568
443
|
return synthesisError(`interpreter.installNonce must be a positive u32 integer (<= ${types_ts_1.SOROBAN_LIMITS.u32Max}), got: ${nonce}`);
|
|
569
444
|
}
|
|
570
|
-
const op = opts.interpreter.oracleParams;
|
|
571
|
-
if (op) {
|
|
572
|
-
const MAX_STALE = 600;
|
|
573
|
-
const MAX_DEV = 200;
|
|
574
|
-
if (op.maxStalenessSeconds !== undefined &&
|
|
575
|
-
(!isPositiveInt(op.maxStalenessSeconds) || op.maxStalenessSeconds > MAX_STALE)) {
|
|
576
|
-
return synthesisError(`interpreter.oracleParams.maxStalenessSeconds must be a positive integer <= ${MAX_STALE} (tighten-only), got: ${op.maxStalenessSeconds}`);
|
|
577
|
-
}
|
|
578
|
-
if (op.maxDeviationBps !== undefined &&
|
|
579
|
-
(!isPositiveInt(op.maxDeviationBps) || op.maxDeviationBps > MAX_DEV)) {
|
|
580
|
-
return synthesisError(`interpreter.oracleParams.maxDeviationBps must be a positive integer <= ${MAX_DEV} (tighten-only), got: ${op.maxDeviationBps}`);
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
445
|
}
|
|
584
446
|
return null;
|
|
585
447
|
}
|
|
@@ -633,180 +495,3 @@ function mergeAmbiguities(...lists) {
|
|
|
633
495
|
}
|
|
634
496
|
return out;
|
|
635
497
|
}
|
|
636
|
-
/** Build the permit `EvalContext` the self-verify harness drives. Shape
|
|
637
|
-
* mirrors the intended recorded call so:
|
|
638
|
-
* - `evaluate(predicate, ctx).permit === true` must hold (a failure
|
|
639
|
-
* surfaces as DENY_CASE_FAILURE).
|
|
640
|
-
* - `generateCases(predicate, ctx)` produces a deny battery that reflects
|
|
641
|
-
* the actual recorded move (real amount, args, window start).
|
|
642
|
-
* Amounts are summed per-token over all movements (BigInt, never lossy).
|
|
643
|
-
* `oraclePriceByAsset` contains a price+timestamp satisfying each
|
|
644
|
-
* `oracle_price` leaf so the intended call permits under every bound;
|
|
645
|
-
* the harness mutates those entries (stale / missing / deviation / paused)
|
|
646
|
-
* to exercise the ORACLE_* deny paths. */
|
|
647
|
-
function buildPermitContext(tx, scope, topLevel, userResponses, predicate) {
|
|
648
|
-
const amountByToken = {};
|
|
649
|
-
const totals = new Map();
|
|
650
|
-
for (const m of tx.tokenMovements) {
|
|
651
|
-
const current = totals.get(m.token) ?? 0n;
|
|
652
|
-
totals.set(m.token, current + BigInt(m.amount));
|
|
653
|
-
}
|
|
654
|
-
for (const [token, total] of totals) {
|
|
655
|
-
amountByToken[token] = total.toString();
|
|
656
|
-
}
|
|
657
|
-
const warnings = [];
|
|
658
|
-
const oraclePriceByAsset = oracleSatisfyingPrices(predicate, tx.fetchedAt, warnings);
|
|
659
|
-
const ctx = {
|
|
660
|
-
contract: scope.contract,
|
|
661
|
-
fn: topLevel.fn,
|
|
662
|
-
args: topLevel.args.map(cloneScVal),
|
|
663
|
-
atLedger: tx.ledgerSequence,
|
|
664
|
-
nowSeconds: tx.fetchedAt,
|
|
665
|
-
amountByToken,
|
|
666
|
-
windowSpentByToken: {},
|
|
667
|
-
invocationCountByWindow: {},
|
|
668
|
-
oraclePriceByAsset,
|
|
669
|
-
};
|
|
670
|
-
if (userResponses?.validUntilLedger !== undefined) {
|
|
671
|
-
ctx.validUntilLedger = userResponses.validUntilLedger;
|
|
672
|
-
}
|
|
673
|
-
return { ctx, warnings };
|
|
674
|
-
}
|
|
675
|
-
function cloneScVal(value, depth = 0) {
|
|
676
|
-
// Clone top-level shells so the harness can mutate deny cases without
|
|
677
|
-
// aliasing the recorded call. Recursion bounded by MAX_SCVAL_CLONE_DEPTH so
|
|
678
|
-
// a hand-crafted nested-vec cannot RangeError the JS stack; the over-depth
|
|
679
|
-
// branch throws a ToolError-shaped error the envelope converts to
|
|
680
|
-
// `{ok:false, error}`.
|
|
681
|
-
if (value.type === 'vec') {
|
|
682
|
-
if (depth >= types_ts_1.MAX_SCVAL_CLONE_DEPTH) {
|
|
683
|
-
throw cloneDepthError(value);
|
|
684
|
-
}
|
|
685
|
-
return {
|
|
686
|
-
type: 'vec',
|
|
687
|
-
value: value.value.map((v) => cloneScVal(v, depth + 1)),
|
|
688
|
-
};
|
|
689
|
-
}
|
|
690
|
-
return { ...value };
|
|
691
|
-
}
|
|
692
|
-
function cloneDepthError(value) {
|
|
693
|
-
const err = new Error(`ScVal clone depth exceeds MAX_SCVAL_CLONE_DEPTH (${types_ts_1.MAX_SCVAL_CLONE_DEPTH})`);
|
|
694
|
-
err.code = 'SYNTHESIS_ERROR';
|
|
695
|
-
err.severity = 'error';
|
|
696
|
-
err.retryable = false;
|
|
697
|
-
err.depthContext = value.type;
|
|
698
|
-
throw err;
|
|
699
|
-
}
|
|
700
|
-
/** Walk every `oracle_price` leaf and return a price map whose entries satisfy
|
|
701
|
-
* the bound. Timestamp pinned to `nowSeconds` (the recorded `fetchedAt`) so
|
|
702
|
-
* the fresh-oracle deny case in `generateCases` is the only path that flips
|
|
703
|
-
* this map. Negatives clamped at 0 - oracle prices are non-negative on Stellar. */
|
|
704
|
-
function oracleSatisfyingPrices(predicate, nowSeconds, warnings) {
|
|
705
|
-
const out = {};
|
|
706
|
-
visitOracleLeaves(predicate, (asset, op, bound) => {
|
|
707
|
-
let price;
|
|
708
|
-
switch (op) {
|
|
709
|
-
case 'lt':
|
|
710
|
-
case 'gt':
|
|
711
|
-
price = op === 'lt' ? bound - 1n : bound + 1n;
|
|
712
|
-
break;
|
|
713
|
-
case 'lte':
|
|
714
|
-
case 'gte':
|
|
715
|
-
case 'eq':
|
|
716
|
-
price = bound;
|
|
717
|
-
break;
|
|
718
|
-
}
|
|
719
|
-
if (price < 0n)
|
|
720
|
-
price = 0n;
|
|
721
|
-
out[asset] = { price: price.toString(), timestampSeconds: nowSeconds };
|
|
722
|
-
}, (warning) => {
|
|
723
|
-
// Cross-layer L3: oracle-on-right that cannot be normalised is
|
|
724
|
-
// surfaced as a warning, not silently dropped. The Rust interpreter
|
|
725
|
-
// would surface this as `UnsupportedNode`; the warning lets the
|
|
726
|
-
// caller decide whether to fix the predicate shape or accept the
|
|
727
|
-
// over-permissive hole.
|
|
728
|
-
warnings.push(warning.message);
|
|
729
|
-
});
|
|
730
|
-
return out;
|
|
731
|
-
}
|
|
732
|
-
function visitOracleLeaves(node, visit, onWarning) {
|
|
733
|
-
switch (node.op) {
|
|
734
|
-
case 'and':
|
|
735
|
-
case 'or':
|
|
736
|
-
for (const child of node.children)
|
|
737
|
-
visitOracleLeaves(child, visit, onWarning);
|
|
738
|
-
return;
|
|
739
|
-
case 'not':
|
|
740
|
-
visitOracleLeaves(node.child, visit, onWarning);
|
|
741
|
-
return;
|
|
742
|
-
case 'eq':
|
|
743
|
-
case 'lt':
|
|
744
|
-
case 'lte':
|
|
745
|
-
case 'gt':
|
|
746
|
-
case 'gte': {
|
|
747
|
-
const leftLeaf = node.left;
|
|
748
|
-
const rightLeaf = node.right;
|
|
749
|
-
const leftIsOracle = leftLeaf.kind === 'oracle_price';
|
|
750
|
-
const rightIsOracle = rightLeaf.kind === 'oracle_price';
|
|
751
|
-
let oracleAsset;
|
|
752
|
-
let literal;
|
|
753
|
-
if (leftIsOracle) {
|
|
754
|
-
oracleAsset = leftLeaf.asset;
|
|
755
|
-
literal = oracleLiteralFromLeaf(rightLeaf);
|
|
756
|
-
}
|
|
757
|
-
else if (rightIsOracle) {
|
|
758
|
-
oracleAsset = rightLeaf.asset;
|
|
759
|
-
literal = oracleLiteralFromLeaf(leftLeaf);
|
|
760
|
-
}
|
|
761
|
-
if (oracleAsset === undefined || literal === undefined) {
|
|
762
|
-
// Two reasons the case cannot be normalised:
|
|
763
|
-
// - neither side is an oracle leaf (visitor has nothing to do)
|
|
764
|
-
// - oracle-on-right with a non-literal LHS (Rust dispatch would
|
|
765
|
-
// hit UnsupportedNode; the TS model cannot build a permit ctx
|
|
766
|
-
// for it)
|
|
767
|
-
// Only the second is a meaningful warning; the first is a no-op
|
|
768
|
-
// (the visitor was called for a non-oracle comparison). Emit the
|
|
769
|
-
// warning when an oracle IS present on one side but the other
|
|
770
|
-
// side is not a parseable threshold literal.
|
|
771
|
-
if (rightIsOracle) {
|
|
772
|
-
onWarning?.({
|
|
773
|
-
dimension: 'oracle_normalisation_dropped',
|
|
774
|
-
op: node.op,
|
|
775
|
-
asset: rightLeaf.kind === 'oracle_price' ? rightLeaf.asset : '',
|
|
776
|
-
otherKind: leftLeaf.kind,
|
|
777
|
-
message: `oracle-on-right cannot be normalised: the LHS (kind=${leftLeaf.kind}) is not an oracle_threshold literal; the Rust interpreter would surface UnsupportedNode here`,
|
|
778
|
-
});
|
|
779
|
-
}
|
|
780
|
-
return;
|
|
781
|
-
}
|
|
782
|
-
visit(oracleAsset, node.op, literal);
|
|
783
|
-
return;
|
|
784
|
-
}
|
|
785
|
-
case 'in':
|
|
786
|
-
// `in` is pure membership; oracle leaves inside haystacks are
|
|
787
|
-
// forbidden by the position rule, so there's nothing to set up here.
|
|
788
|
-
return;
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
/** Restate an oracle threshold on the normalised 9-dp basis prices use.
|
|
792
|
-
* Thresholds carry their own basis, so reading the digits raw would build
|
|
793
|
-
* a permit context off by 10^(decimals-9) and the intended call would not
|
|
794
|
-
* satisfy its own bound. Mirrors NORMALISED_DECIMALS in oracle.rs. */
|
|
795
|
-
function oracleLiteralFromLeaf(leaf) {
|
|
796
|
-
if (leaf.kind !== 'oracle_threshold')
|
|
797
|
-
return undefined;
|
|
798
|
-
let value;
|
|
799
|
-
try {
|
|
800
|
-
value = BigInt(leaf.value);
|
|
801
|
-
}
|
|
802
|
-
catch {
|
|
803
|
-
return undefined;
|
|
804
|
-
}
|
|
805
|
-
const normalised = 9n;
|
|
806
|
-
const decimals = BigInt(leaf.decimals);
|
|
807
|
-
if (decimals <= normalised)
|
|
808
|
-
return value * 10n ** (normalised - decimals);
|
|
809
|
-
// Floor: a finer-grained threshold has no exact 9-dp representation; the
|
|
810
|
-
// caller offsets by one from here to land on the right side of the bound.
|
|
811
|
-
return value / 10n ** (decimals - normalised);
|
|
812
|
-
}
|