@crediolabs/policy-synth 0.1.4 → 0.1.6
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/LICENSE +21 -0
- package/dist/codegen/template.js +3 -3
- package/dist/record/decode.d.ts +6 -3
- package/dist/record/decode.js +56 -3
- package/dist/record/freshness.d.ts +14 -1
- package/dist/record/freshness.js +32 -2
- package/dist/record/index.d.ts +11 -0
- package/dist/record/index.js +25 -0
- package/dist/record/movements.d.ts +15 -3
- package/dist/record/movements.js +42 -7
- package/dist/run/index.d.ts +43 -0
- package/dist/run/index.js +222 -0
- package/dist/run/schemas.d.ts +2016 -0
- package/dist/run/schemas.js +223 -0
- package/dist/synth/address.d.ts +7 -0
- package/dist/synth/address.js +12 -0
- package/dist/synth/compose-from-recording.d.ts +4 -3
- package/dist/synth/compose-from-recording.js +16 -6
- package/dist/synth/deny-cases.d.ts +12 -2
- package/dist/synth/deny-cases.js +76 -4
- package/dist/synth/evaluate.js +2 -2
- package/dist/synth/index.d.ts +1 -0
- package/dist/synth/index.js +4 -0
- package/dist/synth/minimize.d.ts +1 -1
- package/dist/synth/minimize.js +3 -3
- package/dist/synth/synthesize-from-recording.d.ts +5 -2
- package/dist/synth/synthesize-from-recording.js +175 -69
- package/dist/types.d.ts +41 -1
- package/dist/types.js +17 -0
- package/dist/verify/simulate.js +18 -2
- package/dist/verify/verify.js +18 -2
- package/dist-cjs/adapters/interpreter/adapter.d.ts +38 -0
- package/dist-cjs/adapters/interpreter/adapter.js +527 -0
- package/dist-cjs/adapters/interpreter/index.d.ts +1 -0
- package/dist-cjs/adapters/interpreter/index.js +8 -0
- package/dist-cjs/adapters/oz/adapter.d.ts +20 -0
- package/dist-cjs/adapters/oz/adapter.js +289 -0
- package/dist-cjs/adapters/oz/index.d.ts +1 -0
- package/dist-cjs/adapters/oz/index.js +8 -0
- package/dist-cjs/codegen/compile-gate.d.ts +33 -0
- package/dist-cjs/codegen/compile-gate.js +123 -0
- package/dist-cjs/codegen/index.d.ts +2 -0
- package/dist-cjs/codegen/index.js +14 -0
- package/dist-cjs/codegen/template.d.ts +18 -0
- package/dist-cjs/codegen/template.js +134 -0
- package/dist-cjs/errors.d.ts +37 -0
- package/dist-cjs/errors.js +3 -0
- package/dist-cjs/index.d.ts +11 -0
- package/dist-cjs/index.js +27 -0
- package/dist-cjs/ir/index.d.ts +1 -0
- package/dist-cjs/ir/index.js +3 -0
- package/dist-cjs/ir/types.d.ts +108 -0
- package/dist-cjs/ir/types.js +12 -0
- package/dist-cjs/mandate/index.d.ts +2 -0
- package/dist-cjs/mandate/index.js +6 -0
- package/dist-cjs/mandate/to-ir.d.ts +3 -0
- package/dist-cjs/mandate/to-ir.js +63 -0
- package/dist-cjs/mandate/types.d.ts +20 -0
- package/dist-cjs/mandate/types.js +9 -0
- package/dist-cjs/package.json +3 -0
- package/dist-cjs/predicate/encode.d.ts +10 -0
- package/dist-cjs/predicate/encode.js +252 -0
- package/dist-cjs/predicate/index.d.ts +1 -0
- package/dist-cjs/predicate/index.js +6 -0
- package/dist-cjs/record/decode.d.ts +79 -0
- package/dist-cjs/record/decode.js +441 -0
- package/dist-cjs/record/freshness.d.ts +30 -0
- package/dist-cjs/record/freshness.js +85 -0
- package/dist-cjs/record/index.d.ts +32 -0
- package/dist-cjs/record/index.js +191 -0
- package/dist-cjs/record/movements.d.ts +32 -0
- package/dist-cjs/record/movements.js +227 -0
- package/dist-cjs/record/rpc.d.ts +22 -0
- package/dist-cjs/record/rpc.js +74 -0
- package/dist-cjs/record/validate.d.ts +22 -0
- package/dist-cjs/record/validate.js +63 -0
- package/dist-cjs/registry/identify.d.ts +11 -0
- package/dist-cjs/registry/identify.js +87 -0
- package/dist-cjs/registry/index.d.ts +3 -0
- package/dist-cjs/registry/index.js +15 -0
- package/dist-cjs/registry/known-addresses.d.ts +16 -0
- package/dist-cjs/registry/known-addresses.js +53 -0
- package/dist-cjs/registry/protocols.d.ts +38 -0
- package/dist-cjs/registry/protocols.js +153 -0
- package/dist-cjs/review-card/builder.d.ts +14 -0
- package/dist-cjs/review-card/builder.js +264 -0
- package/dist-cjs/review-card/conflict.d.ts +40 -0
- package/dist-cjs/review-card/conflict.js +114 -0
- package/dist-cjs/review-card/cross-check.d.ts +11 -0
- package/dist-cjs/review-card/cross-check.js +151 -0
- package/dist-cjs/review-card/index.d.ts +3 -0
- package/dist-cjs/review-card/index.js +10 -0
- package/dist-cjs/run/index.d.ts +43 -0
- package/dist-cjs/run/index.js +237 -0
- package/dist-cjs/run/schemas.d.ts +2016 -0
- package/dist-cjs/run/schemas.js +226 -0
- package/dist-cjs/seams/index.d.ts +1 -0
- package/dist-cjs/seams/index.js +3 -0
- package/dist-cjs/seams/types.d.ts +66 -0
- package/dist-cjs/seams/types.js +12 -0
- package/dist-cjs/synth/address.d.ts +7 -0
- package/dist-cjs/synth/address.js +15 -0
- package/dist-cjs/synth/compose-from-recording.d.ts +65 -0
- package/dist-cjs/synth/compose-from-recording.js +366 -0
- package/dist-cjs/synth/deny-cases.d.ts +22 -0
- package/dist-cjs/synth/deny-cases.js +438 -0
- package/dist-cjs/synth/evaluate.d.ts +39 -0
- package/dist-cjs/synth/evaluate.js +428 -0
- package/dist-cjs/synth/harness.d.ts +16 -0
- package/dist-cjs/synth/harness.js +29 -0
- package/dist-cjs/synth/index.d.ts +10 -0
- package/dist-cjs/synth/index.js +28 -0
- package/dist-cjs/synth/lower.d.ts +23 -0
- package/dist-cjs/synth/lower.js +119 -0
- package/dist-cjs/synth/minimize.d.ts +4 -0
- package/dist-cjs/synth/minimize.js +41 -0
- package/dist-cjs/synth/predicate-literals.d.ts +5 -0
- package/dist-cjs/synth/predicate-literals.js +28 -0
- package/dist-cjs/synth/scope.d.ts +26 -0
- package/dist-cjs/synth/scope.js +81 -0
- package/dist-cjs/synth/synthesize-from-mandate.d.ts +5 -0
- package/dist-cjs/synth/synthesize-from-mandate.js +37 -0
- package/dist-cjs/synth/synthesize-from-recording.d.ts +49 -0
- package/dist-cjs/synth/synthesize-from-recording.js +759 -0
- package/dist-cjs/types.d.ts +311 -0
- package/dist-cjs/types.js +55 -0
- package/dist-cjs/verify/envelope.d.ts +15 -0
- package/dist-cjs/verify/envelope.js +23 -0
- package/dist-cjs/verify/index.d.ts +3 -0
- package/dist-cjs/verify/index.js +8 -0
- package/dist-cjs/verify/simulate.d.ts +31 -0
- package/dist-cjs/verify/simulate.js +261 -0
- package/dist-cjs/verify/verify.d.ts +21 -0
- package/dist-cjs/verify/verify.js +192 -0
- package/package.json +43 -3
- package/src/codegen/template.ts +3 -3
- package/src/contracts/policy-template/OZ_POLICY_TRAIT.md +171 -0
- package/src/record/corpus-fixtures.json +532 -0
- package/src/record/decode.ts +63 -10
- package/src/record/freshness.ts +34 -2
- package/src/record/index.ts +40 -0
- package/src/record/movements.ts +40 -7
- package/src/run/index.ts +277 -0
- package/src/run/schemas.ts +250 -0
- package/src/synth/address.ts +14 -0
- package/src/synth/compose-from-recording.ts +20 -9
- package/src/synth/deny-cases.ts +87 -4
- package/src/synth/evaluate.ts +2 -2
- package/src/synth/index.ts +4 -0
- package/src/synth/minimize.ts +7 -3
- package/src/synth/synthesize-from-recording.ts +200 -68
- package/src/types.ts +37 -1
- package/src/verify/simulate.ts +21 -2
- package/src/verify/verify.ts +21 -2
|
@@ -33,21 +33,85 @@
|
|
|
33
33
|
import { createInterpreterAdapter, lowerRuleToPredicate, PLACEHOLDER_INTERPRETER_ADDRESS, } from "../adapters/interpreter/adapter.js";
|
|
34
34
|
import { createOzAdapter } from "../adapters/oz/adapter.js";
|
|
35
35
|
import { encodePredicate } from "../predicate/encode.js";
|
|
36
|
-
import { OZ_LIMITS, SOROBAN_LIMITS, } from "../types.js";
|
|
36
|
+
import { MAX_SCVAL_CLONE_DEPTH, OZ_LIMITS, SOROBAN_LIMITS, } from "../types.js";
|
|
37
37
|
import { composeFromRecording, } from "./compose-from-recording.js";
|
|
38
|
-
import { generateCases } from "./deny-cases.js";
|
|
38
|
+
import { generateCases, ORIGINAL_DIMENSIONS } from "./deny-cases.js";
|
|
39
39
|
import { evaluate } from "./evaluate.js";
|
|
40
40
|
import { runHarness } from "./harness.js";
|
|
41
41
|
import { lower } from "./lower.js";
|
|
42
42
|
import { minimize } from "./minimize.js";
|
|
43
43
|
import { decideScope } from "./scope.js";
|
|
44
44
|
const UNCOVERED_PREFIX = 'Not covered by OZ built-in primitives: ';
|
|
45
|
-
/** Synthesize a ProposedPolicy from a recorded transaction. */
|
|
46
45
|
export function synthesizeFromRecording(tx, opts, ozConfig) {
|
|
46
|
+
// Item 3: ToolError try/catch envelope. Any ToolError-shaped throw (object
|
|
47
|
+
// with a string `.code`) inside the body is converted to a structured
|
|
48
|
+
// `{ok:false, error}`; anything else is rethrown so genuine bugs crash
|
|
49
|
+
// instead of being silently swallowed. The envelope must wrap the entire
|
|
50
|
+
// synthesis body so a thrown `encodePredicate` cap error (PREDICATE_TOO_DEEP,
|
|
51
|
+
// TOO_MANY_LEAVES, etc.) or a depth-cap throw from `cloneScVal` surfaces as
|
|
52
|
+
// a structured ToolError rather than a thrown RangeError.
|
|
53
|
+
try {
|
|
54
|
+
return synthesizeFromRecordingInner(tx, opts, ozConfig);
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
if (isToolErrorShape(e)) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
error: {
|
|
61
|
+
code: e.code,
|
|
62
|
+
message: e.message,
|
|
63
|
+
severity: 'error',
|
|
64
|
+
retryable: false,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
throw e;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** True when `e` is the ToolError-shaped throw the internal pipeline uses to
|
|
72
|
+
* signal a structured failure (e.g. `encodePredicate` cap errors, `cloneScVal`
|
|
73
|
+
* depth-cap throws). Detected by a string `code` field — the only contract
|
|
74
|
+
* the body's internal helpers agree on. */
|
|
75
|
+
function isToolErrorShape(e) {
|
|
76
|
+
if (e === null || typeof e !== 'object')
|
|
77
|
+
return false;
|
|
78
|
+
const code = e.code;
|
|
79
|
+
const message = e.message;
|
|
80
|
+
return typeof code === 'string' && typeof message === 'string';
|
|
81
|
+
}
|
|
82
|
+
/** ToolError-shaped error helper used by the body to surface a structured
|
|
83
|
+
* failure that the envelope converts to `{ok:false, error}`. */
|
|
84
|
+
function throwToolError(code, message) {
|
|
85
|
+
const err = new Error(message);
|
|
86
|
+
err.code = code;
|
|
87
|
+
err.severity = 'error';
|
|
88
|
+
err.retryable = false;
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
/** Synthesize a ProposedPolicy from a recorded transaction. */
|
|
92
|
+
function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
47
93
|
// 0. validate inputs (fail closed - never synthesize from garbage).
|
|
48
94
|
const invalid = validateOptions(opts);
|
|
49
95
|
if (invalid)
|
|
50
96
|
return { ok: false, error: invalid };
|
|
97
|
+
// 0a. per-movement amount validation (item 2). Synthesizing from a recording
|
|
98
|
+
// whose `tokenMovements[].amount` does not match the canonical positive
|
|
99
|
+
// decimal integer format is a malformed-input failure, not a runtime
|
|
100
|
+
// one - reject it here so the downstream `BigInt(m.amount)` in
|
|
101
|
+
// `buildPermitContext` cannot throw a SyntaxError past the envelope.
|
|
102
|
+
for (const [i, m] of tx.tokenMovements.entries()) {
|
|
103
|
+
if (!/^[0-9]+$/.test(m.amount)) {
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
error: {
|
|
107
|
+
code: 'RECORDING_VALIDATION_FAILED',
|
|
108
|
+
message: `tokenMovements[${i}].amount must be a positive decimal integer string, got: ${m.amount}`,
|
|
109
|
+
severity: 'error',
|
|
110
|
+
retryable: false,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
51
115
|
// 1. parseConfidence gate.
|
|
52
116
|
const threshold = opts.confidenceOverride?.threshold ?? tx.parseConfidence.thresholdUsed;
|
|
53
117
|
if (tx.parseConfidence.overall < threshold) {
|
|
@@ -62,6 +126,31 @@ export function synthesizeFromRecording(tx, opts, ozConfig) {
|
|
|
62
126
|
},
|
|
63
127
|
};
|
|
64
128
|
}
|
|
129
|
+
// 1a. Zero-invocation refusal (item 1). A recording with zero contract
|
|
130
|
+
// invocations cleared the parseConfidence gate legitimately (the
|
|
131
|
+
// `denom === 0` short-circuit pins overall to 1.0 for that case), but a
|
|
132
|
+
// policy must scope to an authorized contract call. Refuse before any
|
|
133
|
+
// lower/scope work so the failure is specific and actionable. The
|
|
134
|
+
// recorder's silence is also made visible via `parseConfidence.noInvocations`
|
|
135
|
+
// so consumers can pattern-match without inferring from `invocations: []`
|
|
136
|
+
// next to `overall: 1.0`. The message does NOT ask for an ABI - the
|
|
137
|
+
// failure mode is the recording shape, not decoding coverage.
|
|
138
|
+
const hasNoInvocations = tx.parseConfidence.noInvocations === true || tx.invocations.length === 0;
|
|
139
|
+
if (hasNoInvocations) {
|
|
140
|
+
return {
|
|
141
|
+
ok: false,
|
|
142
|
+
error: {
|
|
143
|
+
code: 'SYNTHESIS_ERROR',
|
|
144
|
+
message: 'Recording contains no contract invocation to scope a policy to. Re-record a transaction that invokes a Soroban contract function (e.g. an SAC/SEP-41 transfer, a Blend yield-claim, or a SoroSwap swap).',
|
|
145
|
+
severity: 'error',
|
|
146
|
+
retryable: false,
|
|
147
|
+
details: {
|
|
148
|
+
invocations: tx.invocations.length,
|
|
149
|
+
noInvocationsMarker: tx.parseConfidence.noInvocations === true,
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
65
154
|
// Bound the recording size fail-closed (defense-in-depth for direct callers;
|
|
66
155
|
// the MCP schema caps this too).
|
|
67
156
|
if (tx.invocations.length > SOROBAN_LIMITS.maxInvocations) {
|
|
@@ -88,9 +177,6 @@ export function synthesizeFromRecording(tx, opts, ozConfig) {
|
|
|
88
177
|
return scopeRes;
|
|
89
178
|
const scope = scopeRes.data;
|
|
90
179
|
if (scope.kind !== 'call_contract') {
|
|
91
|
-
// A default scope on the recording-path is not currently a covered flow;
|
|
92
|
-
// surface SYNTHESIS_ERROR so the orchestrator never silently emits an
|
|
93
|
-
// over-broad default-scoped policy.
|
|
94
180
|
return {
|
|
95
181
|
ok: false,
|
|
96
182
|
error: {
|
|
@@ -126,17 +212,6 @@ export function synthesizeFromRecording(tx, opts, ozConfig) {
|
|
|
126
212
|
}
|
|
127
213
|
// 6. Interpreter compile (opt-in; fail-closed when the user routed real
|
|
128
214
|
// restrictions to the interpreter).
|
|
129
|
-
//
|
|
130
|
-
// When the caller opts in, the interpreter always runs - even for a
|
|
131
|
-
// scope-only interpreter IR (no routed constraints) - because the
|
|
132
|
-
// interpreter adapter lowers `scope.contract` + `scope.method` into a
|
|
133
|
-
// `call_contract == ...` and `call_fn == ...` predicate sibling pair.
|
|
134
|
-
// This is the real per-method enforcement the OZ adapter cannot do.
|
|
135
|
-
//
|
|
136
|
-
// When the caller does NOT opt in: today's behaviour is preserved. The
|
|
137
|
-
// compose step routes every constraint to the OZ IR; OZ flags the ones
|
|
138
|
-
// it cannot lower as `uncovered` and the orchestrator surfaces them as
|
|
139
|
-
// warnings. No interpreter doc is produced.
|
|
140
215
|
const interpreterOpts = opts.interpreter;
|
|
141
216
|
let interpreterPolicyDocument = null;
|
|
142
217
|
let interpreterPolicyRef = null;
|
|
@@ -147,18 +222,6 @@ export function synthesizeFromRecording(tx, opts, ozConfig) {
|
|
|
147
222
|
smartAccountAddress: interpreterOpts.smartAccountAddress,
|
|
148
223
|
...(interpreterOpts.oracleParams ? { oracleParams: interpreterOpts.oracleParams } : {}),
|
|
149
224
|
};
|
|
150
|
-
// The starting PredicateNode the self-verify + minimise pipeline drives.
|
|
151
|
-
// Two paths populate it:
|
|
152
|
-
// - Test seam (`__testPredicateNode` set): use it directly, skipping the
|
|
153
|
-
// adapter's compile(). Production code MUST NOT set the seam; it
|
|
154
|
-
// exists so the pipeline can be exercised on hand-crafted predicates
|
|
155
|
-
// (redundant conjuncts for minimise, deliberately over-broad leaves
|
|
156
|
-
// for DENY_CASE_FAILURE).
|
|
157
|
-
// - Normal: call the interpreter adapter's compile() so the existing
|
|
158
|
-
// enforcement gates (SCOPE_SELF_CALL / ORACLE_LEAF_INVALID_POSITION /
|
|
159
|
-
// ORACLE_PARAMS_OUT_OF_RANGE + uncovered reporting) fire first, then
|
|
160
|
-
// re-derive the pre-encoding PredicateNode via `lowerRuleToPredicate`
|
|
161
|
-
// so minimise + harness see the same shape the encoder will see.
|
|
162
225
|
let startingPredicate = null;
|
|
163
226
|
const testSeam = interpreterOpts.__testPredicateNode;
|
|
164
227
|
if (testSeam !== undefined) {
|
|
@@ -171,11 +234,6 @@ export function synthesizeFromRecording(tx, opts, ozConfig) {
|
|
|
171
234
|
interpreterRes = interpreterAdapter.compile(composed.interpreterIr);
|
|
172
235
|
}
|
|
173
236
|
catch (e) {
|
|
174
|
-
// Interpreter enforcement-gate throws (SCOPE_SELF_CALL,
|
|
175
|
-
// ORACLE_LEAF_INVALID_POSITION, ORACLE_PARAMS_OUT_OF_RANGE). Surface
|
|
176
|
-
// the gate code so the caller can act; never install an OZ-only
|
|
177
|
-
// partial policy - the user explicitly asked for these constraints
|
|
178
|
-
// to be enforced and silently dropping them yields an over-broad rule.
|
|
179
237
|
const code = e.code;
|
|
180
238
|
const allowedCodes = [
|
|
181
239
|
'SCOPE_SELF_CALL',
|
|
@@ -260,15 +318,8 @@ export function synthesizeFromRecording(tx, opts, ozConfig) {
|
|
|
260
318
|
},
|
|
261
319
|
};
|
|
262
320
|
}
|
|
263
|
-
// 6b. Self-verify + minimise.
|
|
264
|
-
// from the recorded tx + synthesised constraints so the harness
|
|
265
|
-
// reasons about the intended call (the only one the user actually
|
|
266
|
-
// recorded). The recorded call MUST permit under the predicate; the
|
|
267
|
-
// generated deny battery MUST all deny.
|
|
321
|
+
// 6b. Self-verify + minimise.
|
|
268
322
|
if (!topLevel) {
|
|
269
|
-
// The orchestrator guarantees this for the call_contract branch above;
|
|
270
|
-
// a default-scoped policy never reaches the interpreter block. Fail
|
|
271
|
-
// closed rather than attempt to self-verify a vague shape.
|
|
272
323
|
return {
|
|
273
324
|
ok: false,
|
|
274
325
|
error: {
|
|
@@ -291,12 +342,10 @@ export function synthesizeFromRecording(tx, opts, ozConfig) {
|
|
|
291
342
|
};
|
|
292
343
|
}
|
|
293
344
|
const permitCtx = buildPermitContext(tx, scope, topLevel, opts.userResponses, startingPredicate);
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const finalPredicate = startingPredicate.op === 'and' ? minimize(startingPredicate, permitCtx) : startingPredicate;
|
|
299
|
-
const harnessCases = generateCases(finalPredicate, permitCtx);
|
|
345
|
+
const finalPredicate = startingPredicate.op === 'and'
|
|
346
|
+
? minimize(startingPredicate, permitCtx, ORIGINAL_DIMENSIONS)
|
|
347
|
+
: startingPredicate;
|
|
348
|
+
const harnessCases = generateCases(finalPredicate, permitCtx, ORIGINAL_DIMENSIONS);
|
|
300
349
|
const harnessResult = runHarness(finalPredicate, harnessCases);
|
|
301
350
|
if (!harnessResult.ok) {
|
|
302
351
|
return {
|
|
@@ -324,14 +373,12 @@ export function synthesizeFromRecording(tx, opts, ozConfig) {
|
|
|
324
373
|
};
|
|
325
374
|
}
|
|
326
375
|
// 6c. Re-encode the (possibly minimised) PredicateNode and stamp the
|
|
327
|
-
// canonical bytes back onto the PolicyDocument + PolicyRef.
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
//
|
|
376
|
+
// canonical bytes back onto the PolicyDocument + PolicyRef. The
|
|
377
|
+
// `encodePredicate` helper throws ToolError-shaped errors on cap
|
|
378
|
+
// breaches (PREDICATE_TOO_DEEP, TOO_MANY_LEAVES, etc.) - the outer
|
|
379
|
+
// envelope converts them to a structured `{ok:false, error}`.
|
|
331
380
|
const { encodedPredicate, predicateHash } = encodePredicate(finalPredicate);
|
|
332
381
|
if (testSeam !== undefined) {
|
|
333
|
-
// Test seam: build the PolicyDocument + PolicyRef now (compile() was
|
|
334
|
-
// skipped). Shape mirrors the adapter's output exactly.
|
|
335
382
|
interpreterPolicyDocument = {
|
|
336
383
|
grammarVersion: 1,
|
|
337
384
|
installNonce: interpreterOpts.installNonce ?? 1,
|
|
@@ -371,16 +418,13 @@ export function synthesizeFromRecording(tx, opts, ozConfig) {
|
|
|
371
418
|
}
|
|
372
419
|
}
|
|
373
420
|
// When the interpreter succeeds, OZ-side `uncovered` warnings that the
|
|
374
|
-
// interpreter actually lowered
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
// interpreter did. Drop those so the user-facing warnings reflect what is
|
|
378
|
-
// still UN-enforced, not what OZ alone could not do.
|
|
421
|
+
// interpreter actually lowered are misleading: OZ really did not lower them,
|
|
422
|
+
// but the interpreter did. Drop those so the user-facing warnings reflect
|
|
423
|
+
// what is still UN-enforced, not what OZ alone could not do.
|
|
379
424
|
const ozUncovered = interpreterPolicyRef
|
|
380
425
|
? compileRes.uncovered.filter((u) => !INTERPRETER_COVERED_OZ_PATTERN.test(u))
|
|
381
426
|
: compileRes.uncovered;
|
|
382
|
-
// 7. Merge into the OZ-shaped ProposedPolicy.
|
|
383
|
-
// [interpreterRef?, ...oz_builtinRefs]; cap = OZ_LIMITS.maxPoliciesPerRule.
|
|
427
|
+
// 7. Merge into the OZ-shaped ProposedPolicy.
|
|
384
428
|
const ozRefs = compileRes.proposed.policyRefs;
|
|
385
429
|
const mergedRefs = [];
|
|
386
430
|
if (interpreterPolicyRef)
|
|
@@ -402,12 +446,24 @@ export function synthesizeFromRecording(tx, opts, ozConfig) {
|
|
|
402
446
|
...compileRes.proposed.contextRule,
|
|
403
447
|
policies: mergedRefs,
|
|
404
448
|
};
|
|
449
|
+
// When nothing installable was synthesised (no interpreter doc AND no OZ
|
|
450
|
+
// policy refs), an empty `policies` array reads as "no restrictions" rather
|
|
451
|
+
// than "I synthesised nothing". Surface that explicitly so the empty result
|
|
452
|
+
// is never mistaken for a permissive policy - the context rule still exists,
|
|
453
|
+
// but it constrains nothing. (Kept as `{ok:true}` so the documented
|
|
454
|
+
// Path-A/Path-B demo behaviour is preserved - see F3.)
|
|
455
|
+
const zeroPolicyWarning = mergedRefs.length === 0 && !interpreterPolicyDocument
|
|
456
|
+
? [
|
|
457
|
+
'No policy constraints were synthesised: the call to this contract is UNCONSTRAINED by this policy. Enable the interpreter (supply a smart account) to enforce the surfaced constraints.',
|
|
458
|
+
]
|
|
459
|
+
: [];
|
|
405
460
|
const proposed = {
|
|
406
461
|
contextRule: mergedContextRule,
|
|
407
462
|
policyDocuments: interpreterPolicyDocument ? [interpreterPolicyDocument] : [],
|
|
408
463
|
policyRefs: mergedRefs,
|
|
409
464
|
parseConfidence: { ...tx.parseConfidence },
|
|
410
465
|
warnings: [
|
|
466
|
+
...zeroPolicyWarning,
|
|
411
467
|
...ozUncovered.map((u) => `${UNCOVERED_PREFIX}${u}`),
|
|
412
468
|
...composed.warnings.map((w) => `${UNCOVERED_PREFIX}${w}`),
|
|
413
469
|
],
|
|
@@ -415,6 +471,9 @@ export function synthesizeFromRecording(tx, opts, ozConfig) {
|
|
|
415
471
|
};
|
|
416
472
|
return { ok: true, data: proposed };
|
|
417
473
|
}
|
|
474
|
+
// Re-export the throwToolError helper for callers that need to surface a
|
|
475
|
+
// ToolError-shaped throw inside the body (e.g. tests).
|
|
476
|
+
export { throwToolError };
|
|
418
477
|
/** OZ-side `uncovered` warning patterns that the interpreter adapter
|
|
419
478
|
* actually lowers when wired in. When the interpreter adapter succeeds, we
|
|
420
479
|
* drop matching entries from the OZ uncovered list so the user-facing
|
|
@@ -453,7 +512,7 @@ function validateOptions(opts) {
|
|
|
453
512
|
return synthesisError(`invocationLimit must be a positive integer, got: ${ur.invocationLimit}`);
|
|
454
513
|
}
|
|
455
514
|
if (ur.limitAmount !== undefined && !isPositiveI128(ur.limitAmount)) {
|
|
456
|
-
return synthesisError(`limitAmount must be a positive i128 decimal string, got: ${ur.limitAmount}`);
|
|
515
|
+
return synthesisError(`limitAmount must be a positive i128 decimal string within [1, ${I128_MAX}] (2^127-1), got: ${ur.limitAmount}`);
|
|
457
516
|
}
|
|
458
517
|
}
|
|
459
518
|
if (opts.interpreter) {
|
|
@@ -461,12 +520,25 @@ function validateOptions(opts) {
|
|
|
461
520
|
if (typeof sa !== 'string' || sa.length === 0) {
|
|
462
521
|
return synthesisError(`interpreter.smartAccountAddress must be a non-empty string, got: ${String(sa)}`);
|
|
463
522
|
}
|
|
523
|
+
// Blocklist placeholder / stub prefixes (item 5). Runs BEFORE the
|
|
524
|
+
// C.../56-char shape check so a fixture/LLM-seam marker is reported
|
|
525
|
+
// with the specific placeholder error (not a generic "must be a
|
|
526
|
+
// C... contract" message). A real install routes a C... contract
|
|
527
|
+
// address derivable from the on-chain account; any
|
|
528
|
+
// 'VERIFY-*' / 'PLACEHOLDER-*' / 'TODO-*' prefix is an LLM seam /
|
|
529
|
+
// fixture marker that must never reach the install payload.
|
|
530
|
+
if (PLACEHOLDER_SMART_ACCOUNT_PREFIX.test(sa)) {
|
|
531
|
+
return synthesisError(`interpreter.smartAccountAddress must not be a placeholder/stub address (matches /${PLACEHOLDER_SMART_ACCOUNT_PREFIX.source}/), got: ${sa}`);
|
|
532
|
+
}
|
|
464
533
|
if (!isContractAddress(sa)) {
|
|
465
534
|
return synthesisError(`interpreter.smartAccountAddress must be a C... Stellar contract address (the on-chain policy-bound account, not the G... source account), got: ${sa}`);
|
|
466
535
|
}
|
|
467
536
|
const nonce = opts.interpreter.installNonce;
|
|
468
|
-
|
|
469
|
-
|
|
537
|
+
// Item 4: installNonce must fit u32 (the on-chain per-rule nonce is a
|
|
538
|
+
// u32; values above SOROBAN_LIMITS.u32Max cannot be installed). Mirrors
|
|
539
|
+
// the validUntilLedger SOROBAN_LIMITS.u32Max check above.
|
|
540
|
+
if (nonce !== undefined && (!isPositiveInt(nonce) || nonce > SOROBAN_LIMITS.u32Max)) {
|
|
541
|
+
return synthesisError(`interpreter.installNonce must be a positive u32 integer (<= ${SOROBAN_LIMITS.u32Max}), got: ${nonce}`);
|
|
470
542
|
}
|
|
471
543
|
const op = opts.interpreter.oracleParams;
|
|
472
544
|
if (op) {
|
|
@@ -484,15 +556,30 @@ function validateOptions(opts) {
|
|
|
484
556
|
}
|
|
485
557
|
return null;
|
|
486
558
|
}
|
|
559
|
+
/** Placeholder/stub smart-account prefixes (item 5). Mirrors the
|
|
560
|
+
* `PLACEHOLDER_INTERPRETER_ADDRESS` marker the interpreter adapter uses for
|
|
561
|
+
* the interpreter-contract strkey; a real install must point at a C...
|
|
562
|
+
* contract address derivable from the on-chain account, never a
|
|
563
|
+
* fixture/LLM-seam marker. */
|
|
564
|
+
const PLACEHOLDER_SMART_ACCOUNT_PREFIX = /^(VERIFY-|PLACEHOLDER-|TODO-)/i;
|
|
565
|
+
/** Maximum value a signed i128 can hold (2^127-1). A limitAmount above this
|
|
566
|
+
* cannot be represented on-chain, so reject it at the synthesis boundary
|
|
567
|
+
* (fail-closed) instead of passing it through as an over-broad spending_limit.
|
|
568
|
+
* Mirrors the SOROBAN_LIMITS.u32Max bound the ledger-sequence fields enforce. */
|
|
569
|
+
const I128_MAX = 2n ** 127n - 1n;
|
|
487
570
|
function isPositiveInt(n) {
|
|
488
571
|
return Number.isInteger(n) && n > 0;
|
|
489
572
|
}
|
|
490
|
-
/** True when `s` is a canonical positive decimal integer
|
|
573
|
+
/** True when `s` is a canonical positive decimal integer inside the signed-i128
|
|
574
|
+
* range [1, 2^127-1]. A value above the i128 ceiling is rejected (fail-closed):
|
|
575
|
+
* it cannot be installed on-chain, and accepting it would emit a spending_limit
|
|
576
|
+
* with an effectively unbounded cap. */
|
|
491
577
|
function isPositiveI128(s) {
|
|
492
578
|
if (!/^[0-9]+$/.test(s))
|
|
493
579
|
return false;
|
|
494
580
|
try {
|
|
495
|
-
|
|
581
|
+
const v = BigInt(s);
|
|
582
|
+
return v > 0n && v <= I128_MAX;
|
|
496
583
|
}
|
|
497
584
|
catch {
|
|
498
585
|
return false;
|
|
@@ -561,18 +648,37 @@ function buildPermitContext(tx, scope, topLevel, userResponses, predicate) {
|
|
|
561
648
|
}
|
|
562
649
|
return ctx;
|
|
563
650
|
}
|
|
564
|
-
function cloneScVal(value) {
|
|
651
|
+
function cloneScVal(value, depth = 0) {
|
|
565
652
|
// EvalContext.args is an ScVal[]; the recorded args are already ScVal-shaped
|
|
566
653
|
// (decoded by the recorder). We clone top-level shells so the harness can
|
|
567
|
-
// mutate deny cases without aliasing the recorded call.
|
|
654
|
+
// mutate deny cases without aliasing the recorded call. Recursion is
|
|
655
|
+
// bounded by MAX_SCVAL_CLONE_DEPTH so a hand-crafted nested-vec payload
|
|
656
|
+
// cannot RangeError the JS stack; the over-depth branch throws a
|
|
657
|
+
// ToolError-shaped error that the `synthesizeFromRecording` envelope
|
|
658
|
+
// (item 3) converts to `{ok:false, error}`.
|
|
568
659
|
if (value.type === 'vec') {
|
|
660
|
+
if (depth >= MAX_SCVAL_CLONE_DEPTH) {
|
|
661
|
+
throw cloneDepthError(value);
|
|
662
|
+
}
|
|
569
663
|
return {
|
|
570
664
|
type: 'vec',
|
|
571
|
-
value: value.value.map(cloneScVal),
|
|
665
|
+
value: value.value.map((v) => cloneScVal(v, depth + 1)),
|
|
572
666
|
};
|
|
573
667
|
}
|
|
574
668
|
return { ...value };
|
|
575
669
|
}
|
|
670
|
+
/** Throw a ToolError-shaped error when the clone exceeds MAX_SCVAL_CLONE_DEPTH.
|
|
671
|
+
* The shape (object with string `code`) is what the `synthesizeFromRecording`
|
|
672
|
+
* envelope detects and converts to a structured `{ok:false, error}`. */
|
|
673
|
+
function cloneDepthError(value) {
|
|
674
|
+
const err = new Error(`ScVal clone depth exceeds MAX_SCVAL_CLONE_DEPTH (${MAX_SCVAL_CLONE_DEPTH})`);
|
|
675
|
+
err.code = 'SYNTHESIS_ERROR';
|
|
676
|
+
err.severity = 'error';
|
|
677
|
+
err.retryable = false;
|
|
678
|
+
// Tag the offending value so failures are diagnosable.
|
|
679
|
+
err.depthContext = value.type;
|
|
680
|
+
throw err;
|
|
681
|
+
}
|
|
576
682
|
/** Walk every `oracle_price` leaf in the predicate and return a price map
|
|
577
683
|
* whose entries satisfy the bound so the intended call permits. The
|
|
578
684
|
* timestamp is pinned to `nowSeconds` (the recorded `fetchedAt`) so the
|
package/dist/types.d.ts
CHANGED
|
@@ -21,6 +21,17 @@ export type ScVal = {
|
|
|
21
21
|
} | {
|
|
22
22
|
type: 'bytes';
|
|
23
23
|
value: string;
|
|
24
|
+
}
|
|
25
|
+
/** Map<Symbol, ScVal> carried forward for SAC/SEP-41 transfer event data,
|
|
26
|
+
* which uses a Map shape (key "amount" -> I128) on Stellar mainnet. The
|
|
27
|
+
* entry key is captured as its symbol/form string so readAmount can route
|
|
28
|
+
* to the conventional field name without re-decoding the full XDR. */
|
|
29
|
+
| {
|
|
30
|
+
type: 'map';
|
|
31
|
+
value: Array<{
|
|
32
|
+
key: string;
|
|
33
|
+
val: ScVal;
|
|
34
|
+
}>;
|
|
24
35
|
} | {
|
|
25
36
|
type: 'other';
|
|
26
37
|
value: string;
|
|
@@ -42,6 +53,23 @@ export declare const SOROBAN_LIMITS: {
|
|
|
42
53
|
* used to convert a spend window in seconds to OZ `period_ledgers`. */
|
|
43
54
|
readonly secondsPerLedger: 5;
|
|
44
55
|
};
|
|
56
|
+
/** Maximum recursion depth for decoding Recursive `ScVal` shapes (vec-in-vec).
|
|
57
|
+
* A real Stellar tx caps nesting well below this; the bound hard-caps a
|
|
58
|
+
* hand-crafted payload so the recorder fails CLOSED (records the over-depth
|
|
59
|
+
* branch as opaque `depth-exceeded`) rather than RangeErroring the JS stack.
|
|
60
|
+
* Mirrors the `MAX_AUTH_TREE_DEPTH = 16` boundary the auth-tree decoder
|
|
61
|
+
* already enforces; lifted to 30 so legitimate nested args (a path vec
|
|
62
|
+
* inside an order vec inside a market-data vec) decode cleanly while a
|
|
63
|
+
* malformed ~10000-deep payload still trips the cap. */
|
|
64
|
+
export declare const MAX_SCVAL_DEPTH: 30;
|
|
65
|
+
/** Maximum recursion depth for cloning nested `ScVal` shapes (vec-in-vec) into
|
|
66
|
+
* the simulator / verify / harness contexts. Mirrors `MAX_SCVAL_DEPTH` above
|
|
67
|
+
* so a hand-crafted nested-vec payload cannot RangeError the JS stack during
|
|
68
|
+
* context building. Over-depth throws a ToolError (caught by the
|
|
69
|
+
* `synthesizeFromRecording` envelope + the simulator/verify boundaries) so
|
|
70
|
+
* the caller gets a structured `{ok:false, error}` instead of a thrown
|
|
71
|
+
* RangeError. */
|
|
72
|
+
export declare const MAX_SCVAL_CLONE_DEPTH: 30;
|
|
45
73
|
/** Predicate-document caps - enforced fail-closed at install by BOTH the synth (TS)
|
|
46
74
|
* AND the interpreter (Rust). Single source: a CI test asserts the two cap sets are
|
|
47
75
|
* byte-identical (TS reads the same JSON manifest the Rust build emits). If they
|
|
@@ -255,7 +283,15 @@ export interface AmbiguityPrompt {
|
|
|
255
283
|
export type AmbiguityCode = 'DURATION_UNSPECIFIED' | 'AMOUNT_BOUND_MISSING' | 'RECIPIENT_ALLOWLIST_EMPTY' | 'FREQUENCY_BOUND_MISSING' | 'ORACLE_ASSET_UNKNOWN' | 'MULTIPLE_UNRELATED_TARGETS';
|
|
256
284
|
/** Structured recording freshness. `overall` is the gate threshold (default 1.0); the
|
|
257
285
|
* breakdown is the diagnostic the user / agent sees when the gate fires. The
|
|
258
|
-
* computation rule is pinned: `overall = 1 - (unknownContracts + opaqueScVals) / total`.
|
|
286
|
+
* computation rule is pinned: `overall = 1 - (unknownContracts + opaqueScVals) / total`.
|
|
287
|
+
*
|
|
288
|
+
* `noInvocations` (item 1) is a recorder-side marker set ONLY when the envelope
|
|
289
|
+
* contained zero `invokeContract` host functions (e.g. a `createContract` or
|
|
290
|
+
* `uploadContractWasm` op). The math is unchanged: that case still scores 1.0
|
|
291
|
+
* via the `denom === 0` guard. The marker exists so a consumer does not have
|
|
292
|
+
* to infer the situation from `invocations.length === 0` next to `overall: 1.0`
|
|
293
|
+
* — a downstream synth refuses such recordings with a specific, actionable
|
|
294
|
+
* error rather than silently producing a useless empty scope. */
|
|
259
295
|
export interface ParseConfidence {
|
|
260
296
|
overall: number;
|
|
261
297
|
knownContracts: string[];
|
|
@@ -268,4 +304,8 @@ export interface ParseConfidence {
|
|
|
268
304
|
type: string;
|
|
269
305
|
}>;
|
|
270
306
|
thresholdUsed: number;
|
|
307
|
+
/** True iff the recorder decoded zero contract invocations. Absent on
|
|
308
|
+
* recordings that pre-date this field; consumers should treat `undefined`
|
|
309
|
+
* as "no marker" and rely on `invocations.length` as the fallback. */
|
|
310
|
+
noInvocations?: true;
|
|
271
311
|
}
|
package/dist/types.js
CHANGED
|
@@ -17,6 +17,23 @@ export const SOROBAN_LIMITS = {
|
|
|
17
17
|
* used to convert a spend window in seconds to OZ `period_ledgers`. */
|
|
18
18
|
secondsPerLedger: 5,
|
|
19
19
|
};
|
|
20
|
+
/** Maximum recursion depth for decoding Recursive `ScVal` shapes (vec-in-vec).
|
|
21
|
+
* A real Stellar tx caps nesting well below this; the bound hard-caps a
|
|
22
|
+
* hand-crafted payload so the recorder fails CLOSED (records the over-depth
|
|
23
|
+
* branch as opaque `depth-exceeded`) rather than RangeErroring the JS stack.
|
|
24
|
+
* Mirrors the `MAX_AUTH_TREE_DEPTH = 16` boundary the auth-tree decoder
|
|
25
|
+
* already enforces; lifted to 30 so legitimate nested args (a path vec
|
|
26
|
+
* inside an order vec inside a market-data vec) decode cleanly while a
|
|
27
|
+
* malformed ~10000-deep payload still trips the cap. */
|
|
28
|
+
export const MAX_SCVAL_DEPTH = 30;
|
|
29
|
+
/** Maximum recursion depth for cloning nested `ScVal` shapes (vec-in-vec) into
|
|
30
|
+
* the simulator / verify / harness contexts. Mirrors `MAX_SCVAL_DEPTH` above
|
|
31
|
+
* so a hand-crafted nested-vec payload cannot RangeError the JS stack during
|
|
32
|
+
* context building. Over-depth throws a ToolError (caught by the
|
|
33
|
+
* `synthesizeFromRecording` envelope + the simulator/verify boundaries) so
|
|
34
|
+
* the caller gets a structured `{ok:false, error}` instead of a thrown
|
|
35
|
+
* RangeError. */
|
|
36
|
+
export const MAX_SCVAL_CLONE_DEPTH = 30;
|
|
20
37
|
/** Predicate-document caps - enforced fail-closed at install by BOTH the synth (TS)
|
|
21
38
|
* AND the interpreter (Rust). Single source: a CI test asserts the two cap sets are
|
|
22
39
|
* byte-identical (TS reads the same JSON manifest the Rust build emits). If they
|
package/dist/verify/simulate.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
// suite to stay in lockstep.
|
|
29
29
|
import { generateCases } from "../synth/deny-cases.js";
|
|
30
30
|
import { evaluate } from "../synth/evaluate.js";
|
|
31
|
+
import { MAX_SCVAL_CLONE_DEPTH } from "../types.js";
|
|
31
32
|
const SIMULATOR_VERSION = 'ts-model-1.0.0';
|
|
32
33
|
/** Replay a recorded transaction against a proposed predicate and emit the
|
|
33
34
|
* `SimulationResult` envelope. The simulator returns the SAME permit
|
|
@@ -177,12 +178,27 @@ function buildPermitContext(predicate, tx, topLevel, opts) {
|
|
|
177
178
|
}
|
|
178
179
|
return ctx;
|
|
179
180
|
}
|
|
180
|
-
function cloneScVal(value) {
|
|
181
|
+
function cloneScVal(value, depth = 0) {
|
|
182
|
+
// Recursion is bounded by MAX_SCVAL_CLONE_DEPTH so a hand-crafted nested-vec
|
|
183
|
+
// payload cannot RangeError the JS stack during context building. Over-depth
|
|
184
|
+
// throws a ToolError-shaped error that the simulator's existing try/catch
|
|
185
|
+
// converts to a structured `{ok:false, error}` (not a thrown RangeError).
|
|
181
186
|
if (value.type === 'vec') {
|
|
182
|
-
|
|
187
|
+
if (depth >= MAX_SCVAL_CLONE_DEPTH) {
|
|
188
|
+
throw cloneDepthError(value);
|
|
189
|
+
}
|
|
190
|
+
return { type: 'vec', value: value.value.map((v) => cloneScVal(v, depth + 1)) };
|
|
183
191
|
}
|
|
184
192
|
return { ...value };
|
|
185
193
|
}
|
|
194
|
+
function cloneDepthError(value) {
|
|
195
|
+
const err = new Error(`ScVal clone depth exceeds MAX_SCVAL_CLONE_DEPTH (${MAX_SCVAL_CLONE_DEPTH})`);
|
|
196
|
+
err.code = 'SIMULATION_ERROR';
|
|
197
|
+
err.severity = 'error';
|
|
198
|
+
err.retryable = false;
|
|
199
|
+
err.depthContext = value.type;
|
|
200
|
+
throw err;
|
|
201
|
+
}
|
|
186
202
|
function visitOracleLeaves(node, visit) {
|
|
187
203
|
switch (node.op) {
|
|
188
204
|
case 'and':
|
package/dist/verify/verify.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
//
|
|
29
29
|
// Determinism: same `(predicate, permitTx)` -> byte-identical verdict.
|
|
30
30
|
import { minimize } from "../synth/minimize.js";
|
|
31
|
+
import { MAX_SCVAL_CLONE_DEPTH } from "../types.js";
|
|
31
32
|
/** Run the static minimality check on a proposed predicate + recorded tx.
|
|
32
33
|
* Returns `{ ok: true }` when the predicate is minimal (every top-level
|
|
33
34
|
* conjunct carries load) and `VERIFICATION_FAILED` when a conjunct could
|
|
@@ -125,12 +126,27 @@ function buildPermitContextForVerify(tx, topLevel, opts) {
|
|
|
125
126
|
}
|
|
126
127
|
return ctx;
|
|
127
128
|
}
|
|
128
|
-
function cloneScVal(value) {
|
|
129
|
+
function cloneScVal(value, depth = 0) {
|
|
130
|
+
// Recursion is bounded by MAX_SCVAL_CLONE_DEPTH so a hand-crafted nested-vec
|
|
131
|
+
// payload cannot RangeError the JS stack during context building. Over-depth
|
|
132
|
+
// throws a ToolError-shaped error that the verifier's existing try/catch
|
|
133
|
+
// converts to a structured `{ok:false, error}` (not a thrown RangeError).
|
|
129
134
|
if (value.type === 'vec') {
|
|
130
|
-
|
|
135
|
+
if (depth >= MAX_SCVAL_CLONE_DEPTH) {
|
|
136
|
+
throw cloneDepthError(value);
|
|
137
|
+
}
|
|
138
|
+
return { type: 'vec', value: value.value.map((v) => cloneScVal(v, depth + 1)) };
|
|
131
139
|
}
|
|
132
140
|
return { ...value };
|
|
133
141
|
}
|
|
142
|
+
function cloneDepthError(value) {
|
|
143
|
+
const err = new Error(`ScVal clone depth exceeds MAX_SCVAL_CLONE_DEPTH (${MAX_SCVAL_CLONE_DEPTH})`);
|
|
144
|
+
err.code = 'VERIFICATION_FAILED';
|
|
145
|
+
err.severity = 'error';
|
|
146
|
+
err.retryable = false;
|
|
147
|
+
err.depthContext = value.type;
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
134
150
|
function fingerprint(node) {
|
|
135
151
|
return JSON.stringify(node, replacer);
|
|
136
152
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { IRPolicyRule } from '../../ir/types.ts';
|
|
2
|
+
import type { CustodyAdapter } from '../../seams/types.ts';
|
|
3
|
+
import type { Network, PredicateNode } from '../../types.ts';
|
|
4
|
+
/** [VERIFY] NOT a real deployed address. The interpreter is a per-network
|
|
5
|
+
* deploy artifact we do not have yet; install is a later phase. */
|
|
6
|
+
export declare const PLACEHOLDER_INTERPRETER_ADDRESS = "VERIFY-interpreter-address";
|
|
7
|
+
/** Wasm-level oracle defaults (mirrors INTERPRETER_INSTALL_PARAMS.md OracleParams).
|
|
8
|
+
* Per-policy overrides may TIGHTEN only - they may never exceed these. */
|
|
9
|
+
export declare const ORACLE_DEFAULTS: {
|
|
10
|
+
readonly maxStalenessSeconds: 600;
|
|
11
|
+
readonly maxDeviationBps: 200;
|
|
12
|
+
};
|
|
13
|
+
export interface InterpreterAdapterConfig {
|
|
14
|
+
network: Network;
|
|
15
|
+
/** Per-rule install nonce (first install = 1; replay-protected). */
|
|
16
|
+
installNonce: number;
|
|
17
|
+
/** The smart account this interpreter policy will be installed against. Used
|
|
18
|
+
* by the self-call gate: any `in`-allowlist containing this address is
|
|
19
|
+
* rejected as `SCOPE_SELF_CALL`. */
|
|
20
|
+
smartAccountAddress: string;
|
|
21
|
+
/** Optional per-policy oracle overrides. Must TIGHTEN vs ORACLE_DEFAULTS
|
|
22
|
+
* (i.e. <= each default). A widening value throws ORACLE_PARAMS_OUT_OF_RANGE
|
|
23
|
+
* at compile time. */
|
|
24
|
+
oracleParams?: {
|
|
25
|
+
maxStalenessSeconds?: number;
|
|
26
|
+
maxDeviationBps?: number;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export declare function createInterpreterAdapter(config: InterpreterAdapterConfig): CustodyAdapter;
|
|
30
|
+
/** Lower a single IR rule to the canonical pre-encoding `PredicateNode`. The
|
|
31
|
+
* orchestrator uses this to wire the self-verify + minimise pipeline: after
|
|
32
|
+
* `compile(ir)` succeeds (`covered === true`, `proposed` set), the
|
|
33
|
+
* orchestrator re-derives the PredicateNode via this helper to drive
|
|
34
|
+
* `minimize` and `runHarness` on the SAME shape the encoder saw. Pure and
|
|
35
|
+
* deterministic: same `rule + config` -> byte-identical PredicateNode. The
|
|
36
|
+
* `uncovered` list the adapter surfaces during compile is NOT re-derived
|
|
37
|
+
* here; callers that need it should use the result of `compile(ir)`. */
|
|
38
|
+
export declare function lowerRuleToPredicate(rule: IRPolicyRule, config: InterpreterAdapterConfig): PredicateNode;
|