@crediolabs/policy-synth 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/interpreter/adapter.js +62 -11
- package/dist/errors.d.ts +1 -1
- package/dist/install/build-add-context-rule.d.ts +48 -0
- package/dist/install/build-add-context-rule.js +304 -0
- package/dist/ir/types.d.ts +4 -0
- package/dist/predicate/decode.d.ts +11 -0
- package/dist/predicate/decode.js +234 -0
- package/dist/predicate/encode.js +79 -14
- package/dist/predicate/from-json.d.ts +4 -0
- package/dist/predicate/from-json.js +113 -0
- package/dist/predicate/index.d.ts +2 -0
- package/dist/predicate/index.js +5 -1
- package/dist/registry/identify.js +21 -2
- package/dist/registry/protocols.d.ts +50 -1
- package/dist/registry/protocols.js +88 -0
- package/dist/review-card/builder.d.ts +13 -0
- package/dist/review-card/builder.js +61 -5
- package/dist/review-card/cross-check.js +50 -3
- package/dist/review-card/index.d.ts +1 -1
- package/dist/review-card/index.js +1 -1
- package/dist/run/index.d.ts +8 -2
- package/dist/run/index.js +2 -1
- package/dist/run/schemas.d.ts +16 -4
- package/dist/run/schemas.js +14 -0
- package/dist/synth/compose-from-recording.d.ts +4 -0
- package/dist/synth/compose-from-recording.js +34 -8
- package/dist/synth/deny-cases.d.ts +4 -1
- package/dist/synth/deny-cases.js +3 -1
- package/dist/synth/evaluate.js +130 -3
- package/dist/synth/permit-context.d.ts +15 -0
- package/dist/synth/permit-context.js +116 -0
- package/dist/synth/synthesize-from-mandate.d.ts +17 -2
- package/dist/synth/synthesize-from-mandate.js +27 -2
- package/dist/synth/synthesize-from-recording.d.ts +25 -1
- package/dist/synth/synthesize-from-recording.js +80 -3
- package/dist/types.d.ts +9 -0
- package/dist-cjs/adapters/interpreter/adapter.js +62 -11
- package/dist-cjs/errors.d.ts +1 -1
- package/dist-cjs/install/build-add-context-rule.d.ts +48 -0
- package/dist-cjs/install/build-add-context-rule.js +308 -0
- package/dist-cjs/ir/types.d.ts +4 -0
- package/dist-cjs/predicate/decode.d.ts +11 -0
- package/dist-cjs/predicate/decode.js +239 -0
- package/dist-cjs/predicate/encode.js +79 -14
- package/dist-cjs/predicate/from-json.d.ts +4 -0
- package/dist-cjs/predicate/from-json.js +116 -0
- package/dist-cjs/predicate/index.d.ts +2 -0
- package/dist-cjs/predicate/index.js +10 -2
- package/dist-cjs/registry/identify.js +20 -1
- package/dist-cjs/registry/protocols.d.ts +50 -1
- package/dist-cjs/registry/protocols.js +89 -1
- package/dist-cjs/review-card/builder.d.ts +13 -0
- package/dist-cjs/review-card/builder.js +62 -5
- package/dist-cjs/review-card/cross-check.js +50 -3
- package/dist-cjs/review-card/index.d.ts +1 -1
- package/dist-cjs/review-card/index.js +2 -1
- package/dist-cjs/run/index.d.ts +8 -2
- package/dist-cjs/run/index.js +2 -1
- package/dist-cjs/run/schemas.d.ts +16 -4
- package/dist-cjs/run/schemas.js +14 -0
- package/dist-cjs/synth/compose-from-recording.d.ts +4 -0
- package/dist-cjs/synth/compose-from-recording.js +34 -8
- package/dist-cjs/synth/deny-cases.d.ts +4 -1
- package/dist-cjs/synth/deny-cases.js +3 -0
- package/dist-cjs/synth/evaluate.js +130 -3
- package/dist-cjs/synth/permit-context.d.ts +15 -0
- package/dist-cjs/synth/permit-context.js +119 -0
- package/dist-cjs/synth/synthesize-from-mandate.d.ts +17 -2
- package/dist-cjs/synth/synthesize-from-mandate.js +27 -2
- package/dist-cjs/synth/synthesize-from-recording.d.ts +25 -1
- package/dist-cjs/synth/synthesize-from-recording.js +80 -3
- package/dist-cjs/types.d.ts +9 -0
- package/package.json +5 -2
- package/src/adapters/interpreter/adapter.ts +72 -11
- package/src/contracts/policy-template/OZ_POLICY_TRAIT.md +28 -3
- package/src/errors.ts +13 -0
- package/src/install/build-add-context-rule.ts +429 -0
- package/src/ir/types.ts +4 -0
- package/src/predicate/decode.ts +242 -0
- package/src/predicate/encode.ts +110 -13
- package/src/predicate/from-json.ts +124 -0
- package/src/predicate/index.ts +5 -1
- package/src/registry/identify.ts +22 -2
- package/src/registry/protocols.ts +90 -1
- package/src/review-card/builder.ts +57 -5
- package/src/review-card/cross-check.ts +50 -3
- package/src/review-card/index.ts +1 -0
- package/src/run/index.ts +16 -2
- package/src/run/schemas.ts +14 -0
- package/src/synth/compose-from-recording.ts +46 -7
- package/src/synth/deny-cases.ts +3 -1
- package/src/synth/evaluate.ts +131 -3
- package/src/synth/permit-context.ts +137 -0
- package/src/synth/synthesize-from-mandate.ts +40 -4
- package/src/synth/synthesize-from-recording.ts +116 -6
- package/src/types.ts +12 -0
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
type RecordedTransaction,
|
|
53
53
|
SOROBAN_LIMITS,
|
|
54
54
|
} from '../types.ts'
|
|
55
|
+
import type { SimulationResult } from '../verify/envelope.ts'
|
|
55
56
|
import {
|
|
56
57
|
type ComposeOptions,
|
|
57
58
|
type ComposeUserResponses,
|
|
@@ -101,13 +102,36 @@ export interface SynthesizeFromRecordingOptions {
|
|
|
101
102
|
userResponses?: ComposeUserResponses
|
|
102
103
|
confidenceOverride?: { threshold: number }
|
|
103
104
|
interpreter?: InterpreterAdapterOptions
|
|
105
|
+
/** --explain opt-in. When true, the orchestrator attaches the
|
|
106
|
+
* in-memory `PredicateNode` + the corresponding `SimulationResult`
|
|
107
|
+
* to the success envelope so the CLI can render a human-readable
|
|
108
|
+
* review card. Absent or false -> the success envelope is unchanged
|
|
109
|
+
* (byte-identical to today). The flag is ADDITIVE: the existing
|
|
110
|
+
* ProposedPolicy fields (encodedPredicate, predicateHash, etc.) are
|
|
111
|
+
* never altered by enabling explain. */
|
|
112
|
+
explain?: true
|
|
104
113
|
}
|
|
105
114
|
|
|
106
115
|
export function synthesizeFromRecording(
|
|
107
116
|
tx: RecordedTransaction,
|
|
108
117
|
opts: SynthesizeFromRecordingOptions,
|
|
109
118
|
ozConfig: OzAdapterConfig
|
|
110
|
-
): ToolResponse<ProposedPolicy> {
|
|
119
|
+
): ToolResponse<ProposedPolicy> & {
|
|
120
|
+
/** Present iff `opts.explain === true` and the synthesis succeeded. The
|
|
121
|
+
* `predicateTree` is the exact in-memory `PredicateNode` (canonical
|
|
122
|
+
* JSON shape) that was encoded into `proposed.policyDocuments[*].encodedPredicate`
|
|
123
|
+
* - reading the predicate tree from the policy document is therefore
|
|
124
|
+
* unnecessary; the orchestrator carries it through. The `simulation`
|
|
125
|
+
* is the verdict produced by the same self-verify pipeline that already
|
|
126
|
+
* runs in the recording path (runHarness + evaluate), so the value is
|
|
127
|
+
* REAL not synthetic. Absent when the synthesis did not engage the
|
|
128
|
+
* interpreter (no interpreter opts supplied) - in that case the CLI
|
|
129
|
+
* builds a minimal honest SimulationResult downstream. */
|
|
130
|
+
explain?: {
|
|
131
|
+
predicateTree: PredicateNode | null
|
|
132
|
+
simulation: SimulationResult
|
|
133
|
+
}
|
|
134
|
+
} {
|
|
111
135
|
// Item 3: ToolError try/catch envelope. Any ToolError-shaped throw (object
|
|
112
136
|
// with a string `.code`) inside the body is converted to a structured
|
|
113
137
|
// `{ok:false, error}`; anything else is rethrown so genuine bugs crash
|
|
@@ -159,7 +183,12 @@ function synthesizeFromRecordingInner(
|
|
|
159
183
|
tx: RecordedTransaction,
|
|
160
184
|
opts: SynthesizeFromRecordingOptions,
|
|
161
185
|
ozConfig: OzAdapterConfig
|
|
162
|
-
): ToolResponse<ProposedPolicy> {
|
|
186
|
+
): ToolResponse<ProposedPolicy> & {
|
|
187
|
+
explain?: {
|
|
188
|
+
predicateTree: PredicateNode | null
|
|
189
|
+
simulation: SimulationResult
|
|
190
|
+
}
|
|
191
|
+
} {
|
|
163
192
|
// 0. validate inputs (fail closed - never synthesize from garbage).
|
|
164
193
|
const invalid = validateOptions(opts)
|
|
165
194
|
if (invalid) return { ok: false, error: invalid }
|
|
@@ -272,6 +301,15 @@ function synthesizeFromRecordingInner(
|
|
|
272
301
|
}
|
|
273
302
|
const composed = composeFromRecording(facts, scope.contract, topLevel, composeOpts)
|
|
274
303
|
|
|
304
|
+
// --explain hook: capture the in-memory predicate tree + the real
|
|
305
|
+
// self-verify verdict so the CLI can render a faithful review card.
|
|
306
|
+
// The verdict below is built from the SAME runHarness + evaluate that
|
|
307
|
+
// already gates the synthesis (it is not a parallel simulation); the
|
|
308
|
+
// intermediate inputs (harnessCases, evalResult) are otherwise discarded
|
|
309
|
+
// after the gate, so the explain hook reuses them - no extra work.
|
|
310
|
+
let explain: PredicateNode | null = null
|
|
311
|
+
let explainSim: SimulationResult | null = null
|
|
312
|
+
|
|
275
313
|
// 5. OZ compile (always runs).
|
|
276
314
|
const ozAdapter = createOzAdapter(ozConfig)
|
|
277
315
|
const compileRes = ozAdapter.compile(composed.ir)
|
|
@@ -449,7 +487,6 @@ function synthesizeFromRecordingInner(
|
|
|
449
487
|
},
|
|
450
488
|
}
|
|
451
489
|
}
|
|
452
|
-
|
|
453
490
|
const evalResult = evaluate(finalPredicate, permitCtx)
|
|
454
491
|
if (!evalResult.permit) {
|
|
455
492
|
return {
|
|
@@ -464,6 +501,37 @@ function synthesizeFromRecordingInner(
|
|
|
464
501
|
}
|
|
465
502
|
}
|
|
466
503
|
|
|
504
|
+
// --explain capture: the interpreter path already produced the real
|
|
505
|
+
// self-verify verdict (runHarness passed, evalResult.permit is true).
|
|
506
|
+
// Build the SimulationResult from those outputs so the CLI card
|
|
507
|
+
// quotes the SAME verdict that gated the synthesis. We re-evaluate
|
|
508
|
+
// each deny case to surface its concrete reason; the harness only
|
|
509
|
+
// records whether the got-matches-expected boundary held.
|
|
510
|
+
if (opts.explain) {
|
|
511
|
+
explain = finalPredicate
|
|
512
|
+
const evaluatedCases: SimulationResult['evaluatedCases'] = [
|
|
513
|
+
{
|
|
514
|
+
dimension: 'permit',
|
|
515
|
+
outcome: evalResult.permit ? 'permit' : 'deny',
|
|
516
|
+
reason: 'matches recorded call',
|
|
517
|
+
},
|
|
518
|
+
]
|
|
519
|
+
for (const deny of harnessCases.denies) {
|
|
520
|
+
const r = evaluate(finalPredicate, deny.ctx)
|
|
521
|
+
evaluatedCases.push({
|
|
522
|
+
dimension: deny.dimension,
|
|
523
|
+
outcome: r.permit ? 'permit' : 'deny',
|
|
524
|
+
reason: r.permit ? 'no matching deny' : r.reason,
|
|
525
|
+
})
|
|
526
|
+
}
|
|
527
|
+
explainSim = {
|
|
528
|
+
permit: { tx: 'permit' },
|
|
529
|
+
evaluatedCases,
|
|
530
|
+
backend: 'ts-model',
|
|
531
|
+
simulatorVersion: 'ts-model-1.0.0',
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
467
535
|
// 6c. Re-encode the (possibly minimised) PredicateNode and stamp the
|
|
468
536
|
// canonical bytes back onto the PolicyDocument + PolicyRef. The
|
|
469
537
|
// `encodePredicate` helper throws ToolError-shaped errors on cap
|
|
@@ -566,7 +634,38 @@ function synthesizeFromRecordingInner(
|
|
|
566
634
|
],
|
|
567
635
|
ambiguities: mergeAmbiguities(composed.ambiguities, scope.ambiguities),
|
|
568
636
|
}
|
|
569
|
-
|
|
637
|
+
// --explain success envelope. The interpreter path populated
|
|
638
|
+
// `explain` + `explainSim` from the real self-verify verdict above;
|
|
639
|
+
// the OZ-only path did not (no predicate tree exists). When opts.explain
|
|
640
|
+
// is set and the OZ-only path ran, construct the minimal honest
|
|
641
|
+
// SimulationResult: the verdict is NOT a passing simulation - the
|
|
642
|
+
// interpreter was never engaged, so permit is deny with a truthful
|
|
643
|
+
// reason and evaluatedCases is empty.
|
|
644
|
+
const envelope: ToolResponse<ProposedPolicy> & {
|
|
645
|
+
explain?: {
|
|
646
|
+
predicateTree: PredicateNode | null
|
|
647
|
+
simulation: SimulationResult
|
|
648
|
+
}
|
|
649
|
+
} = { ok: true, data: proposed }
|
|
650
|
+
if (opts.explain) {
|
|
651
|
+
if (explainSim) {
|
|
652
|
+
envelope.explain = { predicateTree: explain, simulation: explainSim }
|
|
653
|
+
} else {
|
|
654
|
+
envelope.explain = {
|
|
655
|
+
predicateTree: null,
|
|
656
|
+
simulation: {
|
|
657
|
+
permit: {
|
|
658
|
+
tx: 'deny',
|
|
659
|
+
reason: 'No self-verification was performed (interpreter adapter was not engaged)',
|
|
660
|
+
},
|
|
661
|
+
evaluatedCases: [],
|
|
662
|
+
backend: 'ts-model',
|
|
663
|
+
simulatorVersion: 'not-run',
|
|
664
|
+
},
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return envelope
|
|
570
669
|
}
|
|
571
670
|
|
|
572
671
|
// Re-export the throwToolError helper for callers that need to surface a
|
|
@@ -905,11 +1004,22 @@ function visitOracleLeaves(
|
|
|
905
1004
|
}
|
|
906
1005
|
}
|
|
907
1006
|
|
|
1007
|
+
/** An oracle threshold restated on the normalised 9-dp basis prices use.
|
|
1008
|
+
* Thresholds carry their own basis, so reading the digits raw would build a
|
|
1009
|
+
* permit context off by 10^(decimals-9) and the intended call would not
|
|
1010
|
+
* satisfy its own bound. Mirrors NORMALISED_DECIMALS in oracle.rs. */
|
|
908
1011
|
function oracleLiteralFromLeaf(leaf: PredicateLeaf): bigint | undefined {
|
|
909
|
-
if (leaf.kind !== '
|
|
1012
|
+
if (leaf.kind !== 'oracle_threshold') return undefined
|
|
1013
|
+
let value: bigint
|
|
910
1014
|
try {
|
|
911
|
-
|
|
1015
|
+
value = BigInt(leaf.value)
|
|
912
1016
|
} catch {
|
|
913
1017
|
return undefined
|
|
914
1018
|
}
|
|
1019
|
+
const normalised = 9n
|
|
1020
|
+
const decimals = BigInt(leaf.decimals)
|
|
1021
|
+
if (decimals <= normalised) return value * 10n ** (normalised - decimals)
|
|
1022
|
+
// Floor: a finer-grained threshold has no exact 9-dp representation; the
|
|
1023
|
+
// caller offsets by one from here to land on the right side of the bound.
|
|
1024
|
+
return value / 10n ** (decimals - normalised)
|
|
915
1025
|
}
|
package/src/types.ts
CHANGED
|
@@ -155,6 +155,13 @@ export type PredicateLeaf =
|
|
|
155
155
|
// recorded ScVal type. A non-vec arg, missing element, missing field, or
|
|
156
156
|
// type mismatch all DENY (fail-closed).
|
|
157
157
|
| { kind: 'call_arg_field'; index: number; element: number; field: string }
|
|
158
|
+
// `call_arg_scaled(index, num, den)` evaluates to `args[index] * num / den`
|
|
159
|
+
// (truncating toward zero). The slippage-floor leaf: a swap policy
|
|
160
|
+
// expresses `call_arg[out] >= call_arg_scaled(in, num, den)` as "the
|
|
161
|
+
// minimum output must be at least this ratio of the input". `num`/`den`
|
|
162
|
+
// are decimal strings on i128 (mirrors `literal_i128`); the contract
|
|
163
|
+
// refuses `den == 0` and `num <= 0` / `den <= 0` at install.
|
|
164
|
+
| { kind: 'call_arg_scaled'; index: number; num: string; den: string }
|
|
158
165
|
// `call_sub_invocation[i]` is NOT in v1 grammar. OZ `Policy::enforce`
|
|
159
166
|
// receives one `Context`, not a sub-invocation tree; sub-invocations live under
|
|
160
167
|
// `InvokerContractAuthEntry::Contract(SubContractInvocation)`, not in the
|
|
@@ -166,6 +173,11 @@ export type PredicateLeaf =
|
|
|
166
173
|
| { kind: 'valid_until' }
|
|
167
174
|
| { kind: 'invocation_count_in_window'; windowSecs: number }
|
|
168
175
|
| { kind: 'oracle_price'; asset: string } // Stellar Address (SAC address for SEP-41; XLM uses its network SAC)
|
|
176
|
+
// The right-hand side of an oracle comparison. It carries its own decimal
|
|
177
|
+
// basis because the contract cannot recover it: prices normalise to 9 dp, so
|
|
178
|
+
// a raw 14-dp threshold is ~10^5 too large and `price <= threshold` becomes
|
|
179
|
+
// trivially true. `value` is a decimal string on i128, like `literal_i128`.
|
|
180
|
+
| { kind: 'oracle_threshold'; value: string; decimals: number }
|
|
169
181
|
// Literal leaves: bare ScVal on the wire (no selector-tuple wrapper). Right-hand side of
|
|
170
182
|
// comparisons, elements of `in` haystacks, and operands to future arithmetic nodes.
|
|
171
183
|
| { kind: 'literal_address'; value: string }
|