@crediolabs/policy-synth 0.1.5 → 0.1.7
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/record/decode.js +43 -0
- 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 +13 -1
- package/dist/run/index.js +76 -18
- package/dist/run/schemas.d.ts +13 -0
- package/dist/run/schemas.js +15 -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 +58 -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.js +54 -6
- package/dist/types.d.ts +24 -1
- package/dist-cjs/record/decode.js +43 -0
- package/dist-cjs/record/freshness.d.ts +14 -1
- package/dist-cjs/record/freshness.js +32 -2
- package/dist-cjs/record/index.d.ts +11 -0
- package/dist-cjs/record/index.js +25 -0
- package/dist-cjs/record/movements.d.ts +15 -3
- package/dist-cjs/record/movements.js +42 -7
- package/dist-cjs/run/index.d.ts +13 -1
- package/dist-cjs/run/index.js +76 -17
- package/dist-cjs/run/schemas.d.ts +13 -0
- package/dist-cjs/run/schemas.js +15 -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 +4 -3
- package/dist-cjs/synth/compose-from-recording.js +16 -6
- package/dist-cjs/synth/deny-cases.d.ts +12 -2
- package/dist-cjs/synth/deny-cases.js +60 -2
- package/dist-cjs/synth/index.d.ts +1 -0
- package/dist-cjs/synth/index.js +6 -1
- package/dist-cjs/synth/minimize.d.ts +1 -1
- package/dist-cjs/synth/minimize.js +3 -3
- package/dist-cjs/synth/synthesize-from-recording.js +53 -5
- package/dist-cjs/types.d.ts +24 -1
- package/package.json +1 -1
- package/src/contracts/policy-template/OZ_POLICY_TRAIT.md +171 -0
- package/src/record/corpus-fixtures.json +532 -0
- package/src/record/decode.ts +42 -0
- 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 +80 -16
- package/src/run/schemas.ts +15 -0
- package/src/synth/address.ts +14 -0
- package/src/synth/compose-from-recording.ts +20 -9
- package/src/synth/deny-cases.ts +66 -2
- package/src/synth/index.ts +4 -0
- package/src/synth/minimize.ts +7 -3
- package/src/synth/synthesize-from-recording.ts +59 -6
- package/src/types.ts +18 -1
|
@@ -57,7 +57,7 @@ import {
|
|
|
57
57
|
type ComposeUserResponses,
|
|
58
58
|
composeFromRecording,
|
|
59
59
|
} from './compose-from-recording.ts'
|
|
60
|
-
import { generateCases } from './deny-cases.ts'
|
|
60
|
+
import { generateCases, ORIGINAL_DIMENSIONS } from './deny-cases.ts'
|
|
61
61
|
import { type EvalContext, evaluate } from './evaluate.ts'
|
|
62
62
|
import { runHarness } from './harness.ts'
|
|
63
63
|
import { lower } from './lower.ts'
|
|
@@ -198,6 +198,33 @@ function synthesizeFromRecordingInner(
|
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
// 1a. Zero-invocation refusal (item 1). A recording with zero contract
|
|
202
|
+
// invocations cleared the parseConfidence gate legitimately (the
|
|
203
|
+
// `denom === 0` short-circuit pins overall to 1.0 for that case), but a
|
|
204
|
+
// policy must scope to an authorized contract call. Refuse before any
|
|
205
|
+
// lower/scope work so the failure is specific and actionable. The
|
|
206
|
+
// recorder's silence is also made visible via `parseConfidence.noInvocations`
|
|
207
|
+
// so consumers can pattern-match without inferring from `invocations: []`
|
|
208
|
+
// next to `overall: 1.0`. The message does NOT ask for an ABI - the
|
|
209
|
+
// failure mode is the recording shape, not decoding coverage.
|
|
210
|
+
const hasNoInvocations = tx.parseConfidence.noInvocations === true || tx.invocations.length === 0
|
|
211
|
+
if (hasNoInvocations) {
|
|
212
|
+
return {
|
|
213
|
+
ok: false,
|
|
214
|
+
error: {
|
|
215
|
+
code: 'SYNTHESIS_ERROR',
|
|
216
|
+
message:
|
|
217
|
+
'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).',
|
|
218
|
+
severity: 'error',
|
|
219
|
+
retryable: false,
|
|
220
|
+
details: {
|
|
221
|
+
invocations: tx.invocations.length,
|
|
222
|
+
noInvocationsMarker: tx.parseConfidence.noInvocations === true,
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
201
228
|
// Bound the recording size fail-closed (defense-in-depth for direct callers;
|
|
202
229
|
// the MCP schema caps this too).
|
|
203
230
|
if (tx.invocations.length > SOROBAN_LIMITS.maxInvocations) {
|
|
@@ -404,9 +431,11 @@ function synthesizeFromRecordingInner(
|
|
|
404
431
|
const permitCtx = buildPermitContext(tx, scope, topLevel, opts.userResponses, startingPredicate)
|
|
405
432
|
|
|
406
433
|
const finalPredicate: PredicateNode =
|
|
407
|
-
startingPredicate.op === 'and'
|
|
434
|
+
startingPredicate.op === 'and'
|
|
435
|
+
? minimize(startingPredicate, permitCtx, ORIGINAL_DIMENSIONS)
|
|
436
|
+
: startingPredicate
|
|
408
437
|
|
|
409
|
-
const harnessCases = generateCases(finalPredicate, permitCtx)
|
|
438
|
+
const harnessCases = generateCases(finalPredicate, permitCtx, ORIGINAL_DIMENSIONS)
|
|
410
439
|
const harnessResult = runHarness(finalPredicate, harnessCases)
|
|
411
440
|
if (!harnessResult.ok) {
|
|
412
441
|
return {
|
|
@@ -512,12 +541,26 @@ function synthesizeFromRecordingInner(
|
|
|
512
541
|
policies: mergedRefs,
|
|
513
542
|
}
|
|
514
543
|
|
|
544
|
+
// When nothing installable was synthesised (no interpreter doc AND no OZ
|
|
545
|
+
// policy refs), an empty `policies` array reads as "no restrictions" rather
|
|
546
|
+
// than "I synthesised nothing". Surface that explicitly so the empty result
|
|
547
|
+
// is never mistaken for a permissive policy - the context rule still exists,
|
|
548
|
+
// but it constrains nothing. (Kept as `{ok:true}` so the documented
|
|
549
|
+
// Path-A/Path-B demo behaviour is preserved - see F3.)
|
|
550
|
+
const zeroPolicyWarning =
|
|
551
|
+
mergedRefs.length === 0 && !interpreterPolicyDocument
|
|
552
|
+
? [
|
|
553
|
+
'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.',
|
|
554
|
+
]
|
|
555
|
+
: []
|
|
556
|
+
|
|
515
557
|
const proposed: ProposedPolicy = {
|
|
516
558
|
contextRule: mergedContextRule,
|
|
517
559
|
policyDocuments: interpreterPolicyDocument ? [interpreterPolicyDocument] : [],
|
|
518
560
|
policyRefs: mergedRefs,
|
|
519
561
|
parseConfidence: { ...tx.parseConfidence },
|
|
520
562
|
warnings: [
|
|
563
|
+
...zeroPolicyWarning,
|
|
521
564
|
...ozUncovered.map((u) => `${UNCOVERED_PREFIX}${u}`),
|
|
522
565
|
...composed.warnings.map((w) => `${UNCOVERED_PREFIX}${w}`),
|
|
523
566
|
],
|
|
@@ -579,7 +622,7 @@ function validateOptions(opts: SynthesizeFromRecordingOptions): ToolError | null
|
|
|
579
622
|
}
|
|
580
623
|
if (ur.limitAmount !== undefined && !isPositiveI128(ur.limitAmount)) {
|
|
581
624
|
return synthesisError(
|
|
582
|
-
`limitAmount must be a positive i128 decimal string, got: ${ur.limitAmount}`
|
|
625
|
+
`limitAmount must be a positive i128 decimal string within [1, ${I128_MAX}] (2^127-1), got: ${ur.limitAmount}`
|
|
583
626
|
)
|
|
584
627
|
}
|
|
585
628
|
}
|
|
@@ -648,15 +691,25 @@ function validateOptions(opts: SynthesizeFromRecordingOptions): ToolError | null
|
|
|
648
691
|
* fixture/LLM-seam marker. */
|
|
649
692
|
const PLACEHOLDER_SMART_ACCOUNT_PREFIX = /^(VERIFY-|PLACEHOLDER-|TODO-)/i
|
|
650
693
|
|
|
694
|
+
/** Maximum value a signed i128 can hold (2^127-1). A limitAmount above this
|
|
695
|
+
* cannot be represented on-chain, so reject it at the synthesis boundary
|
|
696
|
+
* (fail-closed) instead of passing it through as an over-broad spending_limit.
|
|
697
|
+
* Mirrors the SOROBAN_LIMITS.u32Max bound the ledger-sequence fields enforce. */
|
|
698
|
+
const I128_MAX = 2n ** 127n - 1n
|
|
699
|
+
|
|
651
700
|
function isPositiveInt(n: number): boolean {
|
|
652
701
|
return Number.isInteger(n) && n > 0
|
|
653
702
|
}
|
|
654
703
|
|
|
655
|
-
/** True when `s` is a canonical positive decimal integer
|
|
704
|
+
/** True when `s` is a canonical positive decimal integer inside the signed-i128
|
|
705
|
+
* range [1, 2^127-1]. A value above the i128 ceiling is rejected (fail-closed):
|
|
706
|
+
* it cannot be installed on-chain, and accepting it would emit a spending_limit
|
|
707
|
+
* with an effectively unbounded cap. */
|
|
656
708
|
function isPositiveI128(s: string): boolean {
|
|
657
709
|
if (!/^[0-9]+$/.test(s)) return false
|
|
658
710
|
try {
|
|
659
|
-
|
|
711
|
+
const v = BigInt(s)
|
|
712
|
+
return v > 0n && v <= I128_MAX
|
|
660
713
|
} catch {
|
|
661
714
|
return false
|
|
662
715
|
}
|
package/src/types.ts
CHANGED
|
@@ -11,6 +11,11 @@ export type ScVal =
|
|
|
11
11
|
| { type: 'symbol'; value: string }
|
|
12
12
|
| { type: 'vec'; value: ScVal[] }
|
|
13
13
|
| { type: 'bytes'; value: string }
|
|
14
|
+
/** Map<Symbol, ScVal> carried forward for SAC/SEP-41 transfer event data,
|
|
15
|
+
* which uses a Map shape (key "amount" -> I128) on Stellar mainnet. The
|
|
16
|
+
* entry key is captured as its symbol/form string so readAmount can route
|
|
17
|
+
* to the conventional field name without re-decoding the full XDR. */
|
|
18
|
+
| { type: 'map'; value: Array<{ key: string; val: ScVal }> }
|
|
14
19
|
| { type: 'other'; value: string }
|
|
15
20
|
|
|
16
21
|
/** OZ Accounts framework hard limits (verified against canonical OZ source). */
|
|
@@ -241,7 +246,15 @@ export type AmbiguityCode =
|
|
|
241
246
|
|
|
242
247
|
/** Structured recording freshness. `overall` is the gate threshold (default 1.0); the
|
|
243
248
|
* breakdown is the diagnostic the user / agent sees when the gate fires. The
|
|
244
|
-
* computation rule is pinned: `overall = 1 - (unknownContracts + opaqueScVals) / total`.
|
|
249
|
+
* computation rule is pinned: `overall = 1 - (unknownContracts + opaqueScVals) / total`.
|
|
250
|
+
*
|
|
251
|
+
* `noInvocations` (item 1) is a recorder-side marker set ONLY when the envelope
|
|
252
|
+
* contained zero `invokeContract` host functions (e.g. a `createContract` or
|
|
253
|
+
* `uploadContractWasm` op). The math is unchanged: that case still scores 1.0
|
|
254
|
+
* via the `denom === 0` guard. The marker exists so a consumer does not have
|
|
255
|
+
* to infer the situation from `invocations.length === 0` next to `overall: 1.0`
|
|
256
|
+
* — a downstream synth refuses such recordings with a specific, actionable
|
|
257
|
+
* error rather than silently producing a useless empty scope. */
|
|
245
258
|
export interface ParseConfidence {
|
|
246
259
|
overall: number // 0..1
|
|
247
260
|
knownContracts: string[]
|
|
@@ -251,4 +264,8 @@ export interface ParseConfidence {
|
|
|
251
264
|
}>
|
|
252
265
|
opaqueScVals: Array<{ path: string; type: string }>
|
|
253
266
|
thresholdUsed: number // 1.0 by default; > 0 only via explicit confidence_override
|
|
267
|
+
/** True iff the recorder decoded zero contract invocations. Absent on
|
|
268
|
+
* recordings that pre-date this field; consumers should treat `undefined`
|
|
269
|
+
* as "no marker" and rely on `invocations.length` as the fallback. */
|
|
270
|
+
noInvocations?: true
|
|
254
271
|
}
|