@crediolabs/policy-synth 0.1.17 → 0.2.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 +44 -246
- package/dist/adapters/interpreter/adapter.d.ts +6 -7
- package/dist/adapters/interpreter/adapter.js +28 -48
- package/dist/adapters/oz/adapter.js +12 -13
- package/dist/codegen/compile-gate.js +5 -0
- package/dist/codegen/template.js +17 -0
- package/dist/install/authority-overlap.d.ts +134 -0
- package/dist/install/authority-overlap.js +0 -0
- package/dist/install/build-add-context-rule.d.ts +8 -0
- package/dist/install/build-add-context-rule.js +17 -46
- package/dist/install/build-install-policy.d.ts +12 -5
- package/dist/install/build-install-policy.js +32 -23
- package/dist/install/build-merge-policy.d.ts +70 -0
- package/dist/install/build-merge-policy.js +130 -0
- package/dist/install/get-interpreter-info.js +2 -2
- package/dist/install/index.d.ts +3 -1
- package/dist/install/index.js +8 -1
- package/dist/install/oz-auth.js +5 -3
- package/dist/install/plan-merge-policy.d.ts +49 -0
- package/dist/install/plan-merge-policy.js +86 -0
- package/dist/install/read-account-rules.d.ts +100 -0
- package/dist/install/read-account-rules.js +283 -0
- package/dist/predicate/decode.js +1 -1
- package/dist/predicate/encode.js +171 -10
- package/dist/record/decode.js +5 -6
- package/dist/registry/protocols.d.ts +1 -1
- package/dist/registry/protocols.js +1 -1
- package/dist/review-card/builder.d.ts +6 -0
- package/dist/review-card/builder.js +9 -1
- package/dist/run/index.d.ts +104 -18
- package/dist/run/index.js +359 -75
- package/dist/run/schemas.d.ts +479 -18
- package/dist/run/schemas.js +132 -24
- package/dist/synth/compose-from-recording.d.ts +10 -15
- package/dist/synth/compose-from-recording.js +79 -124
- package/dist/synth/deny-cases.d.ts +5 -0
- package/dist/synth/deny-cases.js +52 -13
- package/dist/synth/evaluate.js +69 -119
- package/dist/synth/harness.d.ts +13 -1
- package/dist/synth/harness.js +22 -1
- package/dist/synth/index.d.ts +1 -1
- package/dist/synth/synthesize-from-recording.d.ts +31 -21
- package/dist/synth/synthesize-from-recording.js +129 -155
- package/dist-cjs/adapters/interpreter/adapter.d.ts +6 -7
- package/dist-cjs/adapters/interpreter/adapter.js +28 -48
- package/dist-cjs/adapters/oz/adapter.js +12 -13
- package/dist-cjs/codegen/compile-gate.js +5 -0
- package/dist-cjs/codegen/template.js +17 -0
- package/dist-cjs/install/authority-overlap.d.ts +134 -0
- package/dist-cjs/install/authority-overlap.js +0 -0
- package/dist-cjs/install/build-add-context-rule.d.ts +8 -0
- package/dist-cjs/install/build-add-context-rule.js +17 -45
- package/dist-cjs/install/build-install-policy.d.ts +12 -5
- package/dist-cjs/install/build-install-policy.js +32 -23
- package/dist-cjs/install/build-merge-policy.d.ts +70 -0
- package/dist-cjs/install/build-merge-policy.js +134 -0
- package/dist-cjs/install/get-interpreter-info.js +2 -2
- package/dist-cjs/install/index.d.ts +3 -1
- package/dist-cjs/install/index.js +24 -3
- package/dist-cjs/install/oz-auth.js +5 -3
- package/dist-cjs/install/plan-merge-policy.d.ts +49 -0
- package/dist-cjs/install/plan-merge-policy.js +90 -0
- package/dist-cjs/install/read-account-rules.d.ts +100 -0
- package/dist-cjs/install/read-account-rules.js +296 -0
- package/dist-cjs/predicate/decode.js +1 -1
- package/dist-cjs/predicate/encode.js +171 -10
- package/dist-cjs/record/decode.js +5 -6
- package/dist-cjs/registry/protocols.d.ts +1 -1
- package/dist-cjs/registry/protocols.js +1 -1
- package/dist-cjs/review-card/builder.d.ts +6 -0
- package/dist-cjs/review-card/builder.js +9 -1
- package/dist-cjs/run/index.d.ts +104 -18
- package/dist-cjs/run/index.js +360 -74
- package/dist-cjs/run/schemas.d.ts +479 -18
- package/dist-cjs/run/schemas.js +133 -25
- package/dist-cjs/synth/compose-from-recording.d.ts +10 -15
- package/dist-cjs/synth/compose-from-recording.js +79 -124
- package/dist-cjs/synth/deny-cases.d.ts +5 -0
- package/dist-cjs/synth/deny-cases.js +52 -13
- package/dist-cjs/synth/evaluate.js +69 -119
- package/dist-cjs/synth/harness.d.ts +13 -1
- package/dist-cjs/synth/harness.js +22 -1
- package/dist-cjs/synth/index.d.ts +1 -1
- package/dist-cjs/synth/synthesize-from-recording.d.ts +31 -21
- package/dist-cjs/synth/synthesize-from-recording.js +129 -153
- package/package.json +1 -1
- package/src/adapters/interpreter/adapter.ts +28 -48
- package/src/adapters/oz/adapter.ts +12 -13
- package/src/codegen/compile-gate.ts +5 -0
- package/src/codegen/template.ts +17 -0
- package/src/install/authority-overlap.ts +0 -0
- package/src/install/build-add-context-rule.ts +29 -56
- package/src/install/build-install-policy.ts +41 -23
- package/src/install/build-merge-policy.ts +219 -0
- package/src/install/get-interpreter-info.ts +2 -2
- package/src/install/index.ts +36 -2
- package/src/install/oz-auth.ts +5 -3
- package/src/install/plan-merge-policy.ts +133 -0
- package/src/install/read-account-rules.ts +376 -0
- package/src/predicate/decode.ts +1 -1
- package/src/predicate/encode.ts +176 -10
- package/src/record/decode.ts +5 -6
- package/src/registry/protocols.ts +1 -1
- package/src/review-card/builder.ts +17 -1
- package/src/run/index.ts +500 -102
- package/src/run/schemas.ts +145 -24
- package/src/synth/compose-from-recording.ts +87 -132
- package/src/synth/deny-cases.ts +59 -13
- package/src/synth/evaluate.ts +70 -118
- package/src/synth/harness.ts +29 -1
- package/src/synth/index.ts +1 -0
- package/src/synth/synthesize-from-recording.ts +209 -192
- package/src/contracts/policy-template/OZ_POLICY_TRAIT.md +0 -196
|
@@ -1,36 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// src/synth/synthesize-from-recording.ts - recording-path orchestrator.
|
|
3
3
|
//
|
|
4
|
-
// `synthesizeFromRecording`
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// the caller may relax via `confidenceOverride.threshold`).
|
|
10
|
-
// 2. `lower(tx)` -> IntentFacts.
|
|
11
|
-
// 3. `decideScope(facts)` -> scope | SCOPE_UNRESOLVED ToolError.
|
|
12
|
-
// 4. `composeFromRecording(facts, scope, opts)` -> { ir (OZ-shape),
|
|
13
|
-
// interpreterIr (predicate-shape), ambiguities, warnings }.
|
|
14
|
-
// 5. `ozAdapter.compile(ir)` -> CompileResult.
|
|
15
|
-
// 6. If `interpreterIr.rules[0].constraints.length > 0` AND the caller
|
|
16
|
-
// opted in to the interpreter adapter:
|
|
17
|
-
// a. Run `interpreterAdapter.compile(interpreterIr)`.
|
|
18
|
-
// b. Throw -> SYNTHESIS_ERROR carrying the gate code
|
|
19
|
-
// (SCOPE_SELF_CALL, ORACLE_LEAF_INVALID_POSITION,
|
|
20
|
-
// ORACLE_PARAMS_OUT_OF_RANGE). Never install an OZ-only partial
|
|
21
|
-
// policy - a warning can be ignored and yields an over-broad rule.
|
|
22
|
-
// c. `covered === false` -> SYNTHESIS_ERROR carrying the uncovered
|
|
23
|
-
// descriptor (the user asked for the interpreter to enforce these
|
|
24
|
-
// constraints; refusing them silently is the audit failure mode).
|
|
25
|
-
// d. `covered === true` -> merge its PolicyDocument + PolicyRef into
|
|
26
|
-
// the OZ-shaped ProposedPolicy; the merged `contextRule.policies`
|
|
27
|
-
// is `[interpreterRef?, ...oz_builtinRefs]` and must satisfy
|
|
28
|
-
// `OZ_LIMITS.maxPoliciesPerRule` (= 5).
|
|
29
|
-
// 7. Assemble ProposedPolicy carrying parseConfidence (from tx), warnings
|
|
30
|
-
// (the OZ `uncovered` markers, prefixed), and ambiguities.
|
|
31
|
-
//
|
|
32
|
-
// Determinism: same (tx, opts, ozConfig) -> byte-identical ProposedPolicy.
|
|
33
|
-
// No randomness, no clock, no globals.
|
|
4
|
+
// `synthesizeFromRecording` INFERS a bounded policy from a `RecordedTransaction`
|
|
5
|
+
// via the same `PolicyIR` + adapter pair used by the deterministic Mandate path.
|
|
6
|
+
// Flow: validate -> parseConfidence gate -> lower -> decideScope -> composeFromRecording
|
|
7
|
+
// -> OZ compile -> (opt-in) interpreter compile + self-verify + minimise -> merge.
|
|
8
|
+
// Same (tx, opts, ozConfig) -> byte-identical ProposedPolicy (no randomness, clock, globals).
|
|
34
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
10
|
exports.synthesizeFromRecording = synthesizeFromRecording;
|
|
36
11
|
exports.throwToolError = throwToolError;
|
|
@@ -47,13 +22,11 @@ const minimize_ts_1 = require("./minimize.js");
|
|
|
47
22
|
const scope_ts_1 = require("./scope.js");
|
|
48
23
|
const UNCOVERED_PREFIX = 'Not covered by OZ built-in primitives: ';
|
|
49
24
|
function synthesizeFromRecording(tx, opts, ozConfig) {
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
// TOO_MANY_LEAVES, etc.) or a depth-cap throw from `cloneScVal` surfaces as
|
|
56
|
-
// a structured ToolError rather than a thrown RangeError.
|
|
25
|
+
// Convert any ToolError-shaped throw (object with a string `.code`) to a
|
|
26
|
+
// structured `{ok:false, error}`; anything else is rethrown so genuine bugs
|
|
27
|
+
// crash instead of being silently swallowed. Wraps the entire body so
|
|
28
|
+
// cap errors (PREDICATE_TOO_DEEP, TOO_MANY_LEAVES) and cloneScVal depth
|
|
29
|
+
// throws surface as structured ToolErrors rather than RangeErrors.
|
|
57
30
|
try {
|
|
58
31
|
return synthesizeFromRecordingInner(tx, opts, ozConfig);
|
|
59
32
|
}
|
|
@@ -72,10 +45,9 @@ function synthesizeFromRecording(tx, opts, ozConfig) {
|
|
|
72
45
|
throw e;
|
|
73
46
|
}
|
|
74
47
|
}
|
|
75
|
-
/** True when `e` is
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* the body's internal helpers agree on. */
|
|
48
|
+
/** True when `e` is a ToolError-shaped throw (object with a string `code`).
|
|
49
|
+
* Detected by string `code` field — the only contract the body's internal
|
|
50
|
+
* helpers agree on. */
|
|
79
51
|
function isToolErrorShape(e) {
|
|
80
52
|
if (e === null || typeof e !== 'object')
|
|
81
53
|
return false;
|
|
@@ -92,17 +64,14 @@ function throwToolError(code, message) {
|
|
|
92
64
|
err.retryable = false;
|
|
93
65
|
throw err;
|
|
94
66
|
}
|
|
95
|
-
/** Synthesize a ProposedPolicy from a recorded transaction. */
|
|
96
67
|
function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
97
68
|
// 0. validate inputs (fail closed - never synthesize from garbage).
|
|
98
69
|
const invalid = validateOptions(opts);
|
|
99
70
|
if (invalid)
|
|
100
71
|
return { ok: false, error: invalid };
|
|
101
|
-
// 0a. per-movement amount validation
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
// one - reject it here so the downstream `BigInt(m.amount)` in
|
|
105
|
-
// `buildPermitContext` cannot throw a SyntaxError past the envelope.
|
|
72
|
+
// 0a. per-movement amount validation. Reject malformed recordings here so
|
|
73
|
+
// the downstream `BigInt(m.amount)` in `buildPermitContext` cannot throw
|
|
74
|
+
// a SyntaxError past the envelope.
|
|
106
75
|
for (const [i, m] of tx.tokenMovements.entries()) {
|
|
107
76
|
if (!/^[0-9]+$/.test(m.amount)) {
|
|
108
77
|
return {
|
|
@@ -130,15 +99,9 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
130
99
|
},
|
|
131
100
|
};
|
|
132
101
|
}
|
|
133
|
-
// 1a. Zero-invocation refusal
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
// policy must scope to an authorized contract call. Refuse before any
|
|
137
|
-
// lower/scope work so the failure is specific and actionable. The
|
|
138
|
-
// recorder's silence is also made visible via `parseConfidence.noInvocations`
|
|
139
|
-
// so consumers can pattern-match without inferring from `invocations: []`
|
|
140
|
-
// next to `overall: 1.0`. The message does NOT ask for an ABI - the
|
|
141
|
-
// failure mode is the recording shape, not decoding coverage.
|
|
102
|
+
// 1a. Zero-invocation refusal. A recording with zero invocations clears
|
|
103
|
+
// parseConfidence legitimately (denom === 0 short-circuits overall to 1.0),
|
|
104
|
+
// but a policy must scope to an authorized contract call. Refuse early.
|
|
142
105
|
const hasNoInvocations = tx.parseConfidence.noInvocations === true || tx.invocations.length === 0;
|
|
143
106
|
if (hasNoInvocations) {
|
|
144
107
|
return {
|
|
@@ -168,9 +131,7 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
168
131
|
},
|
|
169
132
|
};
|
|
170
133
|
}
|
|
171
|
-
// 2. lower.
|
|
172
134
|
const facts = (0, lower_ts_1.lower)(tx);
|
|
173
|
-
// 3. decideScope.
|
|
174
135
|
const scopeRes = (0, scope_ts_1.decideScope)(facts, {
|
|
175
136
|
network: opts.network,
|
|
176
137
|
...(opts.userResponses?.validUntilLedger !== undefined
|
|
@@ -200,11 +161,8 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
200
161
|
};
|
|
201
162
|
const composed = (0, compose_from_recording_ts_1.composeFromRecording)(facts, scope.contract, topLevel, composeOpts);
|
|
202
163
|
// --explain hook: capture the in-memory predicate tree + the real
|
|
203
|
-
// self-verify verdict
|
|
204
|
-
//
|
|
205
|
-
// already gates the synthesis (it is not a parallel simulation); the
|
|
206
|
-
// intermediate inputs (harnessCases, evalResult) are otherwise discarded
|
|
207
|
-
// after the gate, so the explain hook reuses them - no extra work.
|
|
164
|
+
// self-verify verdict (built from the SAME runHarness + evaluate that gated
|
|
165
|
+
// the synthesis, not a parallel simulation).
|
|
208
166
|
let explain = null;
|
|
209
167
|
let explainSim = null;
|
|
210
168
|
// 5. OZ compile (always runs).
|
|
@@ -227,6 +185,10 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
227
185
|
const interpreterOpts = opts.interpreter;
|
|
228
186
|
let interpreterPolicyDocument = null;
|
|
229
187
|
let interpreterPolicyRef = null;
|
|
188
|
+
// Cross-layer L3: declared OUTSIDE the `if (interpreterOpts)` block so the
|
|
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 = [];
|
|
230
192
|
if (interpreterOpts) {
|
|
231
193
|
const interpreterConfig = {
|
|
232
194
|
network: opts.network,
|
|
@@ -235,8 +197,17 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
235
197
|
...(interpreterOpts.oracleParams ? { oracleParams: interpreterOpts.oracleParams } : {}),
|
|
236
198
|
};
|
|
237
199
|
let startingPredicate = null;
|
|
200
|
+
// `__testPredicateNode` is a test-only seam. It is NOT in the public
|
|
201
|
+
// `InterpreterAdapterOptions` type, so a production caller cannot set it
|
|
202
|
+
// without bypassing the type system. We read it via a private cast and
|
|
203
|
+
// enforce a runtime NODE_ENV check so a misconfigured production caller
|
|
204
|
+
// that smuggles it in (any-cast, JSON-driven opt, etc.) is caught here
|
|
205
|
+
// rather than silently overriding the compiled predicate.
|
|
238
206
|
const testSeam = interpreterOpts.__testPredicateNode;
|
|
239
207
|
if (testSeam !== undefined) {
|
|
208
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
209
|
+
throw new Error('synthesizeFromRecording: __testPredicateNode is a test-only seam and is refused outside NODE_ENV=test');
|
|
210
|
+
}
|
|
240
211
|
startingPredicate = testSeam;
|
|
241
212
|
}
|
|
242
213
|
else {
|
|
@@ -353,7 +324,12 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
353
324
|
},
|
|
354
325
|
};
|
|
355
326
|
}
|
|
356
|
-
|
|
327
|
+
// Cross-layer L3: warnings collected from `buildPermitContext` (currently
|
|
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;
|
|
357
333
|
const finalPredicate = startingPredicate.op === 'and'
|
|
358
334
|
? (0, minimize_ts_1.minimize)(startingPredicate, permitCtx, deny_cases_ts_1.ORIGINAL_DIMENSIONS)
|
|
359
335
|
: startingPredicate;
|
|
@@ -384,12 +360,9 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
384
360
|
},
|
|
385
361
|
};
|
|
386
362
|
}
|
|
387
|
-
// --explain capture: the
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
// quotes the SAME verdict that gated the synthesis. We re-evaluate
|
|
391
|
-
// each deny case to surface its concrete reason; the harness only
|
|
392
|
-
// records whether the got-matches-expected boundary held.
|
|
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).
|
|
393
366
|
if (opts.explain) {
|
|
394
367
|
explain = finalPredicate;
|
|
395
368
|
const evaluatedCases = [
|
|
@@ -415,10 +388,9 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
415
388
|
};
|
|
416
389
|
}
|
|
417
390
|
// 6c. Re-encode the (possibly minimised) PredicateNode and stamp the
|
|
418
|
-
// canonical bytes back onto the PolicyDocument + PolicyRef.
|
|
419
|
-
//
|
|
420
|
-
//
|
|
421
|
-
// envelope converts them to a structured `{ok:false, error}`.
|
|
391
|
+
// canonical bytes back onto the PolicyDocument + PolicyRef. Cap breaches
|
|
392
|
+
// (PREDICATE_TOO_DEEP, TOO_MANY_LEAVES) throw ToolError-shaped errors;
|
|
393
|
+
// the outer envelope converts them to structured `{ok:false, error}`.
|
|
422
394
|
const { encodedPredicate, predicateHash } = (0, encode_ts_1.encodePredicate)(finalPredicate);
|
|
423
395
|
if (testSeam !== undefined) {
|
|
424
396
|
interpreterPolicyDocument = {
|
|
@@ -491,9 +463,7 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
491
463
|
// When nothing installable was synthesised (no interpreter doc AND no OZ
|
|
492
464
|
// policy refs), an empty `policies` array reads as "no restrictions" rather
|
|
493
465
|
// than "I synthesised nothing". Surface that explicitly so the empty result
|
|
494
|
-
// is never mistaken for a permissive policy
|
|
495
|
-
// but it constrains nothing. (Kept as `{ok:true}` so the documented
|
|
496
|
-
// Path-A/Path-B demo behaviour is preserved - see F3.)
|
|
466
|
+
// is never mistaken for a permissive policy.
|
|
497
467
|
const zeroPolicyWarning = mergedRefs.length === 0 && !interpreterPolicyDocument
|
|
498
468
|
? [
|
|
499
469
|
'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.',
|
|
@@ -508,16 +478,14 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
508
478
|
...zeroPolicyWarning,
|
|
509
479
|
...ozUncovered.map((u) => `${UNCOVERED_PREFIX}${u}`),
|
|
510
480
|
...composed.warnings.map((w) => `${UNCOVERED_PREFIX}${w}`),
|
|
481
|
+
...permitCtxWarnings,
|
|
511
482
|
],
|
|
512
483
|
ambiguities: mergeAmbiguities(composed.ambiguities, scope.ambiguities),
|
|
513
484
|
};
|
|
514
|
-
// --explain success envelope.
|
|
515
|
-
//
|
|
516
|
-
//
|
|
517
|
-
// is
|
|
518
|
-
// SimulationResult: the verdict is NOT a passing simulation - the
|
|
519
|
-
// interpreter was never engaged, so permit is deny with a truthful
|
|
520
|
-
// reason and evaluatedCases is empty.
|
|
485
|
+
// --explain success envelope. When opts.explain is set and the OZ-only
|
|
486
|
+
// path ran, construct the minimal honest SimulationResult: the verdict
|
|
487
|
+
// is NOT a passing simulation - the interpreter was never engaged, so
|
|
488
|
+
// permit is deny with a truthful reason and evaluatedCases is empty.
|
|
521
489
|
const envelope = { ok: true, data: proposed };
|
|
522
490
|
if (opts.explain) {
|
|
523
491
|
if (explainSim) {
|
|
@@ -540,21 +508,20 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
540
508
|
}
|
|
541
509
|
return envelope;
|
|
542
510
|
}
|
|
543
|
-
/** OZ-side `uncovered` warning patterns
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
*
|
|
547
|
-
*
|
|
548
|
-
*
|
|
549
|
-
* `describeSelector`). */
|
|
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`). */
|
|
550
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/;
|
|
551
518
|
/** Reject non-sane inputs before any policy is synthesized. windowSeconds /
|
|
552
519
|
* validUntilLedger / invocationLimit must be positive integers; limitAmount a
|
|
553
|
-
* positive i128 decimal string; network mainnet|testnet. When the
|
|
554
|
-
*
|
|
555
|
-
*
|
|
556
|
-
*
|
|
557
|
-
*
|
|
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. */
|
|
558
525
|
function validateOptions(opts) {
|
|
559
526
|
if (opts.network !== 'mainnet' && opts.network !== 'testnet') {
|
|
560
527
|
return synthesisError(`network must be 'mainnet' or 'testnet', got: ${String(opts.network)}`);
|
|
@@ -586,13 +553,9 @@ function validateOptions(opts) {
|
|
|
586
553
|
if (typeof sa !== 'string' || sa.length === 0) {
|
|
587
554
|
return synthesisError(`interpreter.smartAccountAddress must be a non-empty string, got: ${String(sa)}`);
|
|
588
555
|
}
|
|
589
|
-
//
|
|
590
|
-
//
|
|
591
|
-
//
|
|
592
|
-
// C... contract" message). A real install routes a C... contract
|
|
593
|
-
// address derivable from the on-chain account; any
|
|
594
|
-
// 'VERIFY-*' / 'PLACEHOLDER-*' / 'TODO-*' prefix is an LLM seam /
|
|
595
|
-
// fixture marker that must never reach the install payload.
|
|
556
|
+
// Block placeholder/stub prefixes BEFORE the C.../56-char shape check so
|
|
557
|
+
// a fixture/LLM-seam marker is reported with the specific placeholder
|
|
558
|
+
// error. 'VERIFY-*' / 'PLACEHOLDER-*' / 'TODO-*' must never reach install.
|
|
596
559
|
if (PLACEHOLDER_SMART_ACCOUNT_PREFIX.test(sa)) {
|
|
597
560
|
return synthesisError(`interpreter.smartAccountAddress must not be a placeholder/stub address (matches /${PLACEHOLDER_SMART_ACCOUNT_PREFIX.source}/), got: ${sa}`);
|
|
598
561
|
}
|
|
@@ -600,9 +563,7 @@ function validateOptions(opts) {
|
|
|
600
563
|
return synthesisError(`interpreter.smartAccountAddress must be a C... Stellar contract address (the on-chain policy-bound account, not the G... source account), got: ${sa}`);
|
|
601
564
|
}
|
|
602
565
|
const nonce = opts.interpreter.installNonce;
|
|
603
|
-
//
|
|
604
|
-
// u32; values above SOROBAN_LIMITS.u32Max cannot be installed). Mirrors
|
|
605
|
-
// the validUntilLedger SOROBAN_LIMITS.u32Max check above.
|
|
566
|
+
// installNonce must fit u32 (the on-chain per-rule nonce is a u32).
|
|
606
567
|
if (nonce !== undefined && (!isPositiveInt(nonce) || nonce > types_ts_1.SOROBAN_LIMITS.u32Max)) {
|
|
607
568
|
return synthesisError(`interpreter.installNonce must be a positive u32 integer (<= ${types_ts_1.SOROBAN_LIMITS.u32Max}), got: ${nonce}`);
|
|
608
569
|
}
|
|
@@ -622,7 +583,7 @@ function validateOptions(opts) {
|
|
|
622
583
|
}
|
|
623
584
|
return null;
|
|
624
585
|
}
|
|
625
|
-
/** Placeholder/stub smart-account prefixes
|
|
586
|
+
/** Placeholder/stub smart-account prefixes. Mirrors the
|
|
626
587
|
* `PLACEHOLDER_INTERPRETER_ADDRESS` marker the interpreter adapter uses for
|
|
627
588
|
* the interpreter-contract strkey; a real install must point at a C...
|
|
628
589
|
* contract address derivable from the on-chain account, never a
|
|
@@ -630,16 +591,15 @@ function validateOptions(opts) {
|
|
|
630
591
|
const PLACEHOLDER_SMART_ACCOUNT_PREFIX = /^(VERIFY-|PLACEHOLDER-|TODO-)/i;
|
|
631
592
|
/** Maximum value a signed i128 can hold (2^127-1). A limitAmount above this
|
|
632
593
|
* cannot be represented on-chain, so reject it at the synthesis boundary
|
|
633
|
-
* (fail-closed) instead of passing it through as an over-broad spending_limit.
|
|
634
|
-
* Mirrors the SOROBAN_LIMITS.u32Max bound the ledger-sequence fields enforce. */
|
|
594
|
+
* (fail-closed) instead of passing it through as an over-broad spending_limit. */
|
|
635
595
|
const I128_MAX = 2n ** 127n - 1n;
|
|
636
596
|
function isPositiveInt(n) {
|
|
637
597
|
return Number.isInteger(n) && n > 0;
|
|
638
598
|
}
|
|
639
|
-
/** True when `s` is a canonical positive decimal integer inside
|
|
640
|
-
*
|
|
641
|
-
*
|
|
642
|
-
*
|
|
599
|
+
/** True when `s` is a canonical positive decimal integer inside [1, 2^127-1].
|
|
600
|
+
* Values above the i128 ceiling are rejected fail-closed: they cannot be
|
|
601
|
+
* installed on-chain, and accepting them would emit a spending_limit with an
|
|
602
|
+
* effectively unbounded cap. */
|
|
643
603
|
function isPositiveI128(s) {
|
|
644
604
|
if (!/^[0-9]+$/.test(s))
|
|
645
605
|
return false;
|
|
@@ -673,20 +633,17 @@ function mergeAmbiguities(...lists) {
|
|
|
673
633
|
}
|
|
674
634
|
return out;
|
|
675
635
|
}
|
|
676
|
-
/** Build the permit `EvalContext` the self-verify harness drives.
|
|
677
|
-
* mirrors the intended recorded call
|
|
678
|
-
*
|
|
679
|
-
*
|
|
680
|
-
* permits the recorded call it was derived from); a failure surfaces
|
|
681
|
-
* as DENY_CASE_FAILURE.
|
|
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).
|
|
682
640
|
* - `generateCases(predicate, ctx)` produces a deny battery that reflects
|
|
683
|
-
* the actual recorded move (real amount,
|
|
684
|
-
* Amounts are summed per-token over all movements
|
|
685
|
-
*
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
* exercise the ORACLE_* deny paths. */
|
|
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. */
|
|
690
647
|
function buildPermitContext(tx, scope, topLevel, userResponses, predicate) {
|
|
691
648
|
const amountByToken = {};
|
|
692
649
|
const totals = new Map();
|
|
@@ -697,7 +654,8 @@ function buildPermitContext(tx, scope, topLevel, userResponses, predicate) {
|
|
|
697
654
|
for (const [token, total] of totals) {
|
|
698
655
|
amountByToken[token] = total.toString();
|
|
699
656
|
}
|
|
700
|
-
const
|
|
657
|
+
const warnings = [];
|
|
658
|
+
const oraclePriceByAsset = oracleSatisfyingPrices(predicate, tx.fetchedAt, warnings);
|
|
701
659
|
const ctx = {
|
|
702
660
|
contract: scope.contract,
|
|
703
661
|
fn: topLevel.fn,
|
|
@@ -712,16 +670,14 @@ function buildPermitContext(tx, scope, topLevel, userResponses, predicate) {
|
|
|
712
670
|
if (userResponses?.validUntilLedger !== undefined) {
|
|
713
671
|
ctx.validUntilLedger = userResponses.validUntilLedger;
|
|
714
672
|
}
|
|
715
|
-
return ctx;
|
|
673
|
+
return { ctx, warnings };
|
|
716
674
|
}
|
|
717
675
|
function cloneScVal(value, depth = 0) {
|
|
718
|
-
//
|
|
719
|
-
//
|
|
720
|
-
//
|
|
721
|
-
//
|
|
722
|
-
//
|
|
723
|
-
// ToolError-shaped error that the `synthesizeFromRecording` envelope
|
|
724
|
-
// (item 3) converts to `{ok:false, error}`.
|
|
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}`.
|
|
725
681
|
if (value.type === 'vec') {
|
|
726
682
|
if (depth >= types_ts_1.MAX_SCVAL_CLONE_DEPTH) {
|
|
727
683
|
throw cloneDepthError(value);
|
|
@@ -733,25 +689,19 @@ function cloneScVal(value, depth = 0) {
|
|
|
733
689
|
}
|
|
734
690
|
return { ...value };
|
|
735
691
|
}
|
|
736
|
-
/** Throw a ToolError-shaped error when the clone exceeds MAX_SCVAL_CLONE_DEPTH.
|
|
737
|
-
* The shape (object with string `code`) is what the `synthesizeFromRecording`
|
|
738
|
-
* envelope detects and converts to a structured `{ok:false, error}`. */
|
|
739
692
|
function cloneDepthError(value) {
|
|
740
693
|
const err = new Error(`ScVal clone depth exceeds MAX_SCVAL_CLONE_DEPTH (${types_ts_1.MAX_SCVAL_CLONE_DEPTH})`);
|
|
741
694
|
err.code = 'SYNTHESIS_ERROR';
|
|
742
695
|
err.severity = 'error';
|
|
743
696
|
err.retryable = false;
|
|
744
|
-
// Tag the offending value so failures are diagnosable.
|
|
745
697
|
err.depthContext = value.type;
|
|
746
698
|
throw err;
|
|
747
699
|
}
|
|
748
|
-
/** Walk every `oracle_price` leaf
|
|
749
|
-
*
|
|
750
|
-
*
|
|
751
|
-
*
|
|
752
|
-
|
|
753
|
-
* Stellar. */
|
|
754
|
-
function oracleSatisfyingPrices(predicate, nowSeconds) {
|
|
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) {
|
|
755
705
|
const out = {};
|
|
756
706
|
visitOracleLeaves(predicate, (asset, op, bound) => {
|
|
757
707
|
let price;
|
|
@@ -769,18 +719,25 @@ function oracleSatisfyingPrices(predicate, nowSeconds) {
|
|
|
769
719
|
if (price < 0n)
|
|
770
720
|
price = 0n;
|
|
771
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);
|
|
772
729
|
});
|
|
773
730
|
return out;
|
|
774
731
|
}
|
|
775
|
-
function visitOracleLeaves(node, visit) {
|
|
732
|
+
function visitOracleLeaves(node, visit, onWarning) {
|
|
776
733
|
switch (node.op) {
|
|
777
734
|
case 'and':
|
|
778
735
|
case 'or':
|
|
779
736
|
for (const child of node.children)
|
|
780
|
-
visitOracleLeaves(child, visit);
|
|
737
|
+
visitOracleLeaves(child, visit, onWarning);
|
|
781
738
|
return;
|
|
782
739
|
case 'not':
|
|
783
|
-
visitOracleLeaves(node.child, visit);
|
|
740
|
+
visitOracleLeaves(node.child, visit, onWarning);
|
|
784
741
|
return;
|
|
785
742
|
case 'eq':
|
|
786
743
|
case 'lt':
|
|
@@ -801,8 +758,27 @@ function visitOracleLeaves(node, visit) {
|
|
|
801
758
|
oracleAsset = rightLeaf.asset;
|
|
802
759
|
literal = oracleLiteralFromLeaf(leftLeaf);
|
|
803
760
|
}
|
|
804
|
-
if (oracleAsset === undefined || literal === undefined)
|
|
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
|
+
}
|
|
805
780
|
return;
|
|
781
|
+
}
|
|
806
782
|
visit(oracleAsset, node.op, literal);
|
|
807
783
|
return;
|
|
808
784
|
}
|
|
@@ -812,9 +788,9 @@ function visitOracleLeaves(node, visit) {
|
|
|
812
788
|
return;
|
|
813
789
|
}
|
|
814
790
|
}
|
|
815
|
-
/**
|
|
816
|
-
* Thresholds carry their own basis, so reading the digits raw would build
|
|
817
|
-
* permit context off by 10^(decimals-9) and the intended call would not
|
|
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
|
|
818
794
|
* satisfy its own bound. Mirrors NORMALISED_DECIMALS in oracle.rs. */
|
|
819
795
|
function oracleLiteralFromLeaf(leaf) {
|
|
820
796
|
if (leaf.kind !== 'oracle_threshold')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crediolabs/policy-synth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|