@crediolabs/policy-synth 0.1.4 → 0.1.5
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 +13 -3
- package/dist/run/index.d.ts +31 -0
- package/dist/run/index.js +164 -0
- package/dist/run/schemas.d.ts +2003 -0
- package/dist/run/schemas.js +215 -0
- package/dist/synth/deny-cases.js +18 -2
- package/dist/synth/evaluate.js +2 -2
- package/dist/synth/synthesize-from-recording.d.ts +5 -2
- package/dist/synth/synthesize-from-recording.js +121 -63
- package/dist/types.d.ts +17 -0
- 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 +398 -0
- package/dist-cjs/record/freshness.d.ts +17 -0
- package/dist-cjs/record/freshness.js +55 -0
- package/dist-cjs/record/index.d.ts +21 -0
- package/dist-cjs/record/index.js +166 -0
- package/dist-cjs/record/movements.d.ts +20 -0
- package/dist-cjs/record/movements.js +192 -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 +31 -0
- package/dist-cjs/run/index.js +178 -0
- package/dist-cjs/run/schemas.d.ts +2003 -0
- package/dist-cjs/run/schemas.js +218 -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/compose-from-recording.d.ts +64 -0
- package/dist-cjs/synth/compose-from-recording.js +356 -0
- package/dist-cjs/synth/deny-cases.d.ts +12 -0
- package/dist-cjs/synth/deny-cases.js +380 -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 +9 -0
- package/dist-cjs/synth/index.js +23 -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 +711 -0
- package/dist-cjs/types.d.ts +288 -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/record/decode.ts +21 -10
- package/src/run/index.ts +213 -0
- package/src/run/schemas.ts +242 -0
- package/src/synth/deny-cases.ts +21 -2
- package/src/synth/evaluate.ts +2 -2
- package/src/synth/synthesize-from-recording.ts +141 -62
- package/src/types.ts +19 -0
- package/src/verify/simulate.ts +21 -2
- package/src/verify/verify.ts +21 -2
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// packages/policy-synth/src/run/schemas.ts
|
|
2
|
+
//
|
|
3
|
+
// Zod schemas mirroring the policy-synth core domain types. These are the
|
|
4
|
+
// public input / output shapes exposed over MCP and the CLI. They are kept
|
|
5
|
+
// hand-written (rather than derived) because the MCP SDK needs a runtime
|
|
6
|
+
// Zod object at the transport boundary; a drift test asserts they stay in
|
|
7
|
+
// step with the TS source of truth.
|
|
8
|
+
//
|
|
9
|
+
// i128 amounts and other large integers are carried as base-10 decimal strings
|
|
10
|
+
// end-to-end (no JS number coercion). Networks are pinned to the same closed
|
|
11
|
+
// set the core defines. The discriminated union on `source` exposes BOTH
|
|
12
|
+
// synthesize_policy front-ends through a single tool input.
|
|
13
|
+
//
|
|
14
|
+
// This module is the SINGLE source of truth for these shapes. The MCP package
|
|
15
|
+
// imports them here so its tool-shape bindings stay in step; the CLI imports
|
|
16
|
+
// them here so it can build the same args envelope the MCP transport builds.
|
|
17
|
+
import { z } from 'zod';
|
|
18
|
+
/** Soroban `valid_until` is a u32 ledger sequence; a value above this cannot be
|
|
19
|
+
* installed on-chain, so reject it at the boundary (fail-closed). */
|
|
20
|
+
const U32_MAX = 4294967295;
|
|
21
|
+
/** Upper bound on top-level invocations in a recorded transaction. A real
|
|
22
|
+
* Stellar tx caps operations well below this; the bound stops a hand-crafted
|
|
23
|
+
* payload from turning one request into an unbounded synthesis (DoS). */
|
|
24
|
+
const MAX_INVOCATIONS = 512;
|
|
25
|
+
export const NetworkSchema = z.enum(['mainnet', 'testnet']);
|
|
26
|
+
/** ScVal subset - normalised subset the synth consumes. Mirrors
|
|
27
|
+
* `ScVal` in packages/policy-synth/src/types.ts. */
|
|
28
|
+
export const ScValSchema = z.lazy(() => z.union([
|
|
29
|
+
z.object({ type: z.literal('address'), value: z.string() }),
|
|
30
|
+
// i128 is SIGNED: real events carry negatives (e.g. a fee-adjustment/refund),
|
|
31
|
+
// so the recorder's own output must round-trip through this schema. u64/u32
|
|
32
|
+
// are unsigned and stay non-negative.
|
|
33
|
+
z.object({ type: z.literal('i128'), value: z.string().regex(/^-?[0-9]+$/) }),
|
|
34
|
+
z.object({ type: z.literal('u64'), value: z.string().regex(/^[0-9]+$/) }),
|
|
35
|
+
z.object({ type: z.literal('u32'), value: z.string().regex(/^[0-9]+$/) }),
|
|
36
|
+
z.object({ type: z.literal('symbol'), value: z.string() }),
|
|
37
|
+
z.object({ type: z.literal('vec'), value: z.array(ScValSchema) }),
|
|
38
|
+
z.object({ type: z.literal('bytes'), value: z.string() }),
|
|
39
|
+
z.object({ type: z.literal('other'), value: z.string() }),
|
|
40
|
+
]));
|
|
41
|
+
/** ContractInvocation mirrors the core. Annotated with an explicit
|
|
42
|
+
* `z.ZodType<unknown>` (like ScValSchema above) so the self-referential
|
|
43
|
+
* `subInvocations` field does not trip TS's circular type inference. */
|
|
44
|
+
export const ContractInvocationSchema = z.object({
|
|
45
|
+
contract: z.string(),
|
|
46
|
+
fn: z.string(),
|
|
47
|
+
args: z.array(ScValSchema),
|
|
48
|
+
subInvocations: z.array(z.lazy(() => ContractInvocationSchema)),
|
|
49
|
+
});
|
|
50
|
+
export const TokenMovementSchema = z.object({
|
|
51
|
+
token: z.string(),
|
|
52
|
+
from: z.string(),
|
|
53
|
+
to: z.string(),
|
|
54
|
+
// The recorder reads the amount straight from the signed i128 event value
|
|
55
|
+
// (record/movements.ts readAmount), so a non-standard token that emits a
|
|
56
|
+
// negative transfer/mint/burn amount round-trips as a negative string. Mirror
|
|
57
|
+
// that here; the synth gate, not the wire schema, decides what to do with it.
|
|
58
|
+
amount: z.string().regex(/^-?[0-9]+$/),
|
|
59
|
+
});
|
|
60
|
+
export const OnChainEventSchema = z.object({
|
|
61
|
+
contract: z.string(),
|
|
62
|
+
topics: z.array(z.string()),
|
|
63
|
+
data: ScValSchema,
|
|
64
|
+
});
|
|
65
|
+
export const ParseConfidenceSchema = z.object({
|
|
66
|
+
overall: z.number().min(0).max(1),
|
|
67
|
+
knownContracts: z.array(z.string()),
|
|
68
|
+
unknownContracts: z.array(z.object({
|
|
69
|
+
contract: z.string(),
|
|
70
|
+
reason: z.enum(['no-abi', 'version-mismatch', 'opaque-result']),
|
|
71
|
+
})),
|
|
72
|
+
opaqueScVals: z.array(z.object({ path: z.string(), type: z.string() })),
|
|
73
|
+
thresholdUsed: z.number().min(0).max(1),
|
|
74
|
+
});
|
|
75
|
+
/** RecordedTransaction mirrors the core RecordedTransaction. The output shape
|
|
76
|
+
* is referenced by name in the tool result structured content; we deliberately
|
|
77
|
+
* type it loosely (`z.unknown()`) on the success path so the core remains the
|
|
78
|
+
* single source of truth for the wire payload. */
|
|
79
|
+
export const RecordedTransactionSchema = z
|
|
80
|
+
.object({
|
|
81
|
+
network: NetworkSchema,
|
|
82
|
+
signers: z.array(z.string()),
|
|
83
|
+
invocations: z.array(ContractInvocationSchema).max(MAX_INVOCATIONS),
|
|
84
|
+
tokenMovements: z.array(TokenMovementSchema),
|
|
85
|
+
events: z.array(OnChainEventSchema),
|
|
86
|
+
authEntries: z.array(z.unknown()),
|
|
87
|
+
ledgerSequence: z.number().int().nonnegative(),
|
|
88
|
+
fetchedAt: z.number().int().nonnegative(),
|
|
89
|
+
parseConfidence: ParseConfidenceSchema,
|
|
90
|
+
sourceAccount: z.string(),
|
|
91
|
+
})
|
|
92
|
+
.passthrough();
|
|
93
|
+
/** MandateSpec mirrors the core MandateSpec. The deterministic Mandate
|
|
94
|
+
* front-end needs no parseConfidence; the tool adapter injects the full
|
|
95
|
+
* confidence after synthesis so the orchestrator can compare. */
|
|
96
|
+
export const MandateSpecSchema = z
|
|
97
|
+
.object({
|
|
98
|
+
chain: z.literal('stellar'),
|
|
99
|
+
contract: z.string(),
|
|
100
|
+
method: z.string().optional(),
|
|
101
|
+
spendingLimit: z
|
|
102
|
+
.object({
|
|
103
|
+
token: z.string(),
|
|
104
|
+
limit: z.string().regex(/^[0-9]+$/),
|
|
105
|
+
windowSeconds: z.number().int().positive(),
|
|
106
|
+
})
|
|
107
|
+
.optional(),
|
|
108
|
+
// A threshold of 0 means "0 approvals", which is not a real M-of-N gate.
|
|
109
|
+
approvalThreshold: z.number().int().positive().optional(),
|
|
110
|
+
recipients: z.array(z.string()).optional(),
|
|
111
|
+
expiry: z
|
|
112
|
+
.object({
|
|
113
|
+
validUntilLedger: z.number().int().positive().max(U32_MAX).optional(),
|
|
114
|
+
validUntilUnixSeconds: z.number().int().positive().optional(),
|
|
115
|
+
})
|
|
116
|
+
.optional(),
|
|
117
|
+
})
|
|
118
|
+
.passthrough();
|
|
119
|
+
/** ComposeUserResponses mirrors the core. */
|
|
120
|
+
export const ComposeUserResponsesSchema = z
|
|
121
|
+
.object({
|
|
122
|
+
windowSeconds: z.number().int().positive().optional(),
|
|
123
|
+
validUntilLedger: z.number().int().positive().max(U32_MAX).optional(),
|
|
124
|
+
limitAmount: z
|
|
125
|
+
.string()
|
|
126
|
+
.regex(/^[0-9]+$/)
|
|
127
|
+
.optional(),
|
|
128
|
+
invocationLimit: z.number().int().positive().optional(),
|
|
129
|
+
})
|
|
130
|
+
.passthrough();
|
|
131
|
+
/** OzAdapterConfig - the per-network OZ built-in instance addresses. */
|
|
132
|
+
export const OzAdapterConfigSchema = z.object({
|
|
133
|
+
network: NetworkSchema,
|
|
134
|
+
instances: z.object({
|
|
135
|
+
spending_limit: z.string(),
|
|
136
|
+
simple_threshold: z.string(),
|
|
137
|
+
weighted_threshold: z.string(),
|
|
138
|
+
}),
|
|
139
|
+
});
|
|
140
|
+
// ===== record_transaction =====
|
|
141
|
+
export const RecordTransactionInputSchema = z
|
|
142
|
+
.object({
|
|
143
|
+
hash: z.string().min(1).optional(),
|
|
144
|
+
xdr: z.string().min(1).optional(),
|
|
145
|
+
network: NetworkSchema,
|
|
146
|
+
confidenceOverride: z.number().min(0).max(1).optional(),
|
|
147
|
+
})
|
|
148
|
+
.refine((v) => !(v.hash && v.xdr), {
|
|
149
|
+
message: 'provide exactly one of `hash` or `xdr`, not both',
|
|
150
|
+
})
|
|
151
|
+
.refine((v) => Boolean(v.hash) || Boolean(v.xdr), {
|
|
152
|
+
message: 'one of `hash` or `xdr` is required',
|
|
153
|
+
});
|
|
154
|
+
// ===== synthesize_policy =====
|
|
155
|
+
//
|
|
156
|
+
// Discriminated union on `source` exposes BOTH front-ends through ONE tool.
|
|
157
|
+
// - `source: 'mandate'` -> calls synthesizeFromMandate
|
|
158
|
+
// - `source: 'recording'` -> calls synthesizeFromRecording
|
|
159
|
+
export const SynthesizePolicyMandateInputSchema = z.object({
|
|
160
|
+
source: z.literal('mandate'),
|
|
161
|
+
mandate: MandateSpecSchema,
|
|
162
|
+
ozConfig: OzAdapterConfigSchema.optional(),
|
|
163
|
+
});
|
|
164
|
+
/** Interpreter opt-in for the recording path. Present -> constraints OZ cannot
|
|
165
|
+
* express (per-method scoping, invocation-count windows, oracle bounds, exact
|
|
166
|
+
* hop paths) lower to a real interpreter predicate document instead of being
|
|
167
|
+
* surfaced as warnings. The core deep-validates `smartAccountAddress` (a C...
|
|
168
|
+
* contract, not the recording's G... source) and the tighten-only oracle
|
|
169
|
+
* bounds; the schema stays light so the core owns the friendly ToolErrors. */
|
|
170
|
+
export const InterpreterOptionsSchema = z.object({
|
|
171
|
+
smartAccountAddress: z.string(),
|
|
172
|
+
installNonce: z.number().int().positive().optional(),
|
|
173
|
+
oracleParams: z
|
|
174
|
+
.object({
|
|
175
|
+
maxStalenessSeconds: z.number().int().positive().optional(),
|
|
176
|
+
maxDeviationBps: z.number().int().positive().optional(),
|
|
177
|
+
})
|
|
178
|
+
.optional(),
|
|
179
|
+
});
|
|
180
|
+
export const SynthesizePolicyRecordingInputSchema = z.object({
|
|
181
|
+
source: z.literal('recording'),
|
|
182
|
+
recordedTx: RecordedTransactionSchema,
|
|
183
|
+
network: NetworkSchema,
|
|
184
|
+
userResponses: ComposeUserResponsesSchema.optional(),
|
|
185
|
+
confidenceOverride: z.object({ threshold: z.number().min(0).max(1) }).optional(),
|
|
186
|
+
interpreter: InterpreterOptionsSchema.optional(),
|
|
187
|
+
ozConfig: OzAdapterConfigSchema.optional(),
|
|
188
|
+
});
|
|
189
|
+
export const SynthesizePolicyInputSchema = z.discriminatedUnion('source', [
|
|
190
|
+
SynthesizePolicyMandateInputSchema,
|
|
191
|
+
SynthesizePolicyRecordingInputSchema,
|
|
192
|
+
]);
|
|
193
|
+
// ===== Error envelope (canonical) =====
|
|
194
|
+
//
|
|
195
|
+
// Mirrors ToolError from packages/policy-synth/src/errors.ts. We use a
|
|
196
|
+
// `z.string()` for `code` (not an enum) because the core's ErrorCode union
|
|
197
|
+
// evolves over time; the transport contract only promises a string code the
|
|
198
|
+
// caller can dispatch on. A drift test asserts the canonical codes still
|
|
199
|
+
// pass through unchanged.
|
|
200
|
+
export const ToolErrorSchema = z
|
|
201
|
+
.object({
|
|
202
|
+
code: z.string(),
|
|
203
|
+
message: z.string(),
|
|
204
|
+
severity: z.enum(['info', 'warning', 'error', 'fatal']),
|
|
205
|
+
retryable: z.boolean(),
|
|
206
|
+
remediation: z
|
|
207
|
+
.object({
|
|
208
|
+
toolCall: z.object({ name: z.string(), args: z.record(z.unknown()) }).optional(),
|
|
209
|
+
userQuestion: z.object({ code: z.string(), question: z.string() }).optional(),
|
|
210
|
+
docsUrl: z.string().optional(),
|
|
211
|
+
})
|
|
212
|
+
.optional(),
|
|
213
|
+
details: z.unknown().optional(),
|
|
214
|
+
})
|
|
215
|
+
.passthrough();
|
package/dist/synth/deny-cases.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MAX_SCVAL_CLONE_DEPTH } from "../types.js";
|
|
1
2
|
import { literalNumericBigInt } from "./predicate-literals.js";
|
|
2
3
|
const ORACLE_CASES = [
|
|
3
4
|
['oracle_stale', 'stale'],
|
|
@@ -353,9 +354,24 @@ function cloneContext(ctx) {
|
|
|
353
354
|
cloned.signerWeights = { ...ctx.signerWeights };
|
|
354
355
|
return cloned;
|
|
355
356
|
}
|
|
356
|
-
function cloneScVal(value) {
|
|
357
|
+
function cloneScVal(value, depth = 0) {
|
|
358
|
+
// Recursion is bounded by MAX_SCVAL_CLONE_DEPTH so a hand-crafted nested-vec
|
|
359
|
+
// payload cannot RangeError the JS stack during deny-case mutation. Over-depth
|
|
360
|
+
// throws a ToolError-shaped error that the `synthesizeFromRecording` envelope
|
|
361
|
+
// (item 3) converts to a structured `{ok:false, error}`.
|
|
357
362
|
if (value.type === 'vec') {
|
|
358
|
-
|
|
363
|
+
if (depth >= MAX_SCVAL_CLONE_DEPTH) {
|
|
364
|
+
throw cloneDepthError(value);
|
|
365
|
+
}
|
|
366
|
+
return { type: 'vec', value: value.value.map((v) => cloneScVal(v, depth + 1)) };
|
|
359
367
|
}
|
|
360
368
|
return { ...value };
|
|
361
369
|
}
|
|
370
|
+
function cloneDepthError(value) {
|
|
371
|
+
const err = new Error(`ScVal clone depth exceeds MAX_SCVAL_CLONE_DEPTH (${MAX_SCVAL_CLONE_DEPTH})`);
|
|
372
|
+
err.code = 'SYNTHESIS_ERROR';
|
|
373
|
+
err.severity = 'error';
|
|
374
|
+
err.retryable = false;
|
|
375
|
+
err.depthContext = value.type;
|
|
376
|
+
throw err;
|
|
377
|
+
}
|
package/dist/synth/evaluate.js
CHANGED
|
@@ -200,7 +200,7 @@ function evalCompare(op, left, right, ctx) {
|
|
|
200
200
|
function evalArgEq(op, actual, right, ctx) {
|
|
201
201
|
// eq(call_arg[i], literal_vec) -> EXACT ordered vector equality
|
|
202
202
|
if (op === 'eq' && right.kind === 'literal_vec') {
|
|
203
|
-
if (
|
|
203
|
+
if (actual?.type !== 'vec')
|
|
204
204
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
205
205
|
return compareVecExact(actual.value, right.elements, ctx)
|
|
206
206
|
? { permit: true }
|
|
@@ -218,7 +218,7 @@ function evalArgEq(op, actual, right, ctx) {
|
|
|
218
218
|
}
|
|
219
219
|
// eq(call_arg[i], literal_i128) -> BigInt compare
|
|
220
220
|
if (op === 'eq' && right.kind === 'literal_i128') {
|
|
221
|
-
if (
|
|
221
|
+
if (actual?.type !== 'i128')
|
|
222
222
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
223
223
|
return BigInt(actual.value) === BigInt(right.value)
|
|
224
224
|
? { permit: true }
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { OzAdapterConfig } from '../adapters/oz/adapter.ts';
|
|
2
|
-
import type { ToolResponse } from '../errors.ts';
|
|
2
|
+
import type { ToolError, ToolResponse } from '../errors.ts';
|
|
3
3
|
import { type Network, type PredicateNode, type ProposedPolicy, type RecordedTransaction } from '../types.ts';
|
|
4
4
|
import { type ComposeUserResponses } from './compose-from-recording.ts';
|
|
5
5
|
/** Per-policy interpreter adapter config the caller opts into. Absent ->
|
|
@@ -42,5 +42,8 @@ export interface SynthesizeFromRecordingOptions {
|
|
|
42
42
|
};
|
|
43
43
|
interpreter?: InterpreterAdapterOptions;
|
|
44
44
|
}
|
|
45
|
-
/** Synthesize a ProposedPolicy from a recorded transaction. */
|
|
46
45
|
export declare function synthesizeFromRecording(tx: RecordedTransaction, opts: SynthesizeFromRecordingOptions, ozConfig: OzAdapterConfig): ToolResponse<ProposedPolicy>;
|
|
46
|
+
/** ToolError-shaped error helper used by the body to surface a structured
|
|
47
|
+
* failure that the envelope converts to `{ok:false, error}`. */
|
|
48
|
+
declare function throwToolError(code: ToolError['code'], message: string): never;
|
|
49
|
+
export { throwToolError };
|
|
@@ -33,7 +33,7 @@
|
|
|
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
38
|
import { generateCases } from "./deny-cases.js";
|
|
39
39
|
import { evaluate } from "./evaluate.js";
|
|
@@ -42,12 +42,76 @@ 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) {
|
|
@@ -88,9 +152,6 @@ export function synthesizeFromRecording(tx, opts, ozConfig) {
|
|
|
88
152
|
return scopeRes;
|
|
89
153
|
const scope = scopeRes.data;
|
|
90
154
|
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
155
|
return {
|
|
95
156
|
ok: false,
|
|
96
157
|
error: {
|
|
@@ -126,17 +187,6 @@ export function synthesizeFromRecording(tx, opts, ozConfig) {
|
|
|
126
187
|
}
|
|
127
188
|
// 6. Interpreter compile (opt-in; fail-closed when the user routed real
|
|
128
189
|
// 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
190
|
const interpreterOpts = opts.interpreter;
|
|
141
191
|
let interpreterPolicyDocument = null;
|
|
142
192
|
let interpreterPolicyRef = null;
|
|
@@ -147,18 +197,6 @@ export function synthesizeFromRecording(tx, opts, ozConfig) {
|
|
|
147
197
|
smartAccountAddress: interpreterOpts.smartAccountAddress,
|
|
148
198
|
...(interpreterOpts.oracleParams ? { oracleParams: interpreterOpts.oracleParams } : {}),
|
|
149
199
|
};
|
|
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
200
|
let startingPredicate = null;
|
|
163
201
|
const testSeam = interpreterOpts.__testPredicateNode;
|
|
164
202
|
if (testSeam !== undefined) {
|
|
@@ -171,11 +209,6 @@ export function synthesizeFromRecording(tx, opts, ozConfig) {
|
|
|
171
209
|
interpreterRes = interpreterAdapter.compile(composed.interpreterIr);
|
|
172
210
|
}
|
|
173
211
|
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
212
|
const code = e.code;
|
|
180
213
|
const allowedCodes = [
|
|
181
214
|
'SCOPE_SELF_CALL',
|
|
@@ -260,15 +293,8 @@ export function synthesizeFromRecording(tx, opts, ozConfig) {
|
|
|
260
293
|
},
|
|
261
294
|
};
|
|
262
295
|
}
|
|
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.
|
|
296
|
+
// 6b. Self-verify + minimise.
|
|
268
297
|
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
298
|
return {
|
|
273
299
|
ok: false,
|
|
274
300
|
error: {
|
|
@@ -291,10 +317,6 @@ export function synthesizeFromRecording(tx, opts, ozConfig) {
|
|
|
291
317
|
};
|
|
292
318
|
}
|
|
293
319
|
const permitCtx = buildPermitContext(tx, scope, topLevel, opts.userResponses, startingPredicate);
|
|
294
|
-
// Top-level `and` predicates are what generateCases + minimize assume;
|
|
295
|
-
// for any other shape we skip minimise (encode as-is) but still assert
|
|
296
|
-
// the permit case permits. An over-broad non-and predicate is still
|
|
297
|
-
// caught by the harness on the next step.
|
|
298
320
|
const finalPredicate = startingPredicate.op === 'and' ? minimize(startingPredicate, permitCtx) : startingPredicate;
|
|
299
321
|
const harnessCases = generateCases(finalPredicate, permitCtx);
|
|
300
322
|
const harnessResult = runHarness(finalPredicate, harnessCases);
|
|
@@ -324,14 +346,12 @@ export function synthesizeFromRecording(tx, opts, ozConfig) {
|
|
|
324
346
|
};
|
|
325
347
|
}
|
|
326
348
|
// 6c. Re-encode the (possibly minimised) PredicateNode and stamp the
|
|
327
|
-
// canonical bytes back onto the PolicyDocument + PolicyRef.
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
//
|
|
349
|
+
// canonical bytes back onto the PolicyDocument + PolicyRef. The
|
|
350
|
+
// `encodePredicate` helper throws ToolError-shaped errors on cap
|
|
351
|
+
// breaches (PREDICATE_TOO_DEEP, TOO_MANY_LEAVES, etc.) - the outer
|
|
352
|
+
// envelope converts them to a structured `{ok:false, error}`.
|
|
331
353
|
const { encodedPredicate, predicateHash } = encodePredicate(finalPredicate);
|
|
332
354
|
if (testSeam !== undefined) {
|
|
333
|
-
// Test seam: build the PolicyDocument + PolicyRef now (compile() was
|
|
334
|
-
// skipped). Shape mirrors the adapter's output exactly.
|
|
335
355
|
interpreterPolicyDocument = {
|
|
336
356
|
grammarVersion: 1,
|
|
337
357
|
installNonce: interpreterOpts.installNonce ?? 1,
|
|
@@ -371,16 +391,13 @@ export function synthesizeFromRecording(tx, opts, ozConfig) {
|
|
|
371
391
|
}
|
|
372
392
|
}
|
|
373
393
|
// 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.
|
|
394
|
+
// interpreter actually lowered are misleading: OZ really did not lower them,
|
|
395
|
+
// but the interpreter did. Drop those so the user-facing warnings reflect
|
|
396
|
+
// what is still UN-enforced, not what OZ alone could not do.
|
|
379
397
|
const ozUncovered = interpreterPolicyRef
|
|
380
398
|
? compileRes.uncovered.filter((u) => !INTERPRETER_COVERED_OZ_PATTERN.test(u))
|
|
381
399
|
: compileRes.uncovered;
|
|
382
|
-
// 7. Merge into the OZ-shaped ProposedPolicy.
|
|
383
|
-
// [interpreterRef?, ...oz_builtinRefs]; cap = OZ_LIMITS.maxPoliciesPerRule.
|
|
400
|
+
// 7. Merge into the OZ-shaped ProposedPolicy.
|
|
384
401
|
const ozRefs = compileRes.proposed.policyRefs;
|
|
385
402
|
const mergedRefs = [];
|
|
386
403
|
if (interpreterPolicyRef)
|
|
@@ -415,6 +432,9 @@ export function synthesizeFromRecording(tx, opts, ozConfig) {
|
|
|
415
432
|
};
|
|
416
433
|
return { ok: true, data: proposed };
|
|
417
434
|
}
|
|
435
|
+
// Re-export the throwToolError helper for callers that need to surface a
|
|
436
|
+
// ToolError-shaped throw inside the body (e.g. tests).
|
|
437
|
+
export { throwToolError };
|
|
418
438
|
/** OZ-side `uncovered` warning patterns that the interpreter adapter
|
|
419
439
|
* actually lowers when wired in. When the interpreter adapter succeeds, we
|
|
420
440
|
* drop matching entries from the OZ uncovered list so the user-facing
|
|
@@ -461,12 +481,25 @@ function validateOptions(opts) {
|
|
|
461
481
|
if (typeof sa !== 'string' || sa.length === 0) {
|
|
462
482
|
return synthesisError(`interpreter.smartAccountAddress must be a non-empty string, got: ${String(sa)}`);
|
|
463
483
|
}
|
|
484
|
+
// Blocklist placeholder / stub prefixes (item 5). Runs BEFORE the
|
|
485
|
+
// C.../56-char shape check so a fixture/LLM-seam marker is reported
|
|
486
|
+
// with the specific placeholder error (not a generic "must be a
|
|
487
|
+
// C... contract" message). A real install routes a C... contract
|
|
488
|
+
// address derivable from the on-chain account; any
|
|
489
|
+
// 'VERIFY-*' / 'PLACEHOLDER-*' / 'TODO-*' prefix is an LLM seam /
|
|
490
|
+
// fixture marker that must never reach the install payload.
|
|
491
|
+
if (PLACEHOLDER_SMART_ACCOUNT_PREFIX.test(sa)) {
|
|
492
|
+
return synthesisError(`interpreter.smartAccountAddress must not be a placeholder/stub address (matches /${PLACEHOLDER_SMART_ACCOUNT_PREFIX.source}/), got: ${sa}`);
|
|
493
|
+
}
|
|
464
494
|
if (!isContractAddress(sa)) {
|
|
465
495
|
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
496
|
}
|
|
467
497
|
const nonce = opts.interpreter.installNonce;
|
|
468
|
-
|
|
469
|
-
|
|
498
|
+
// Item 4: installNonce must fit u32 (the on-chain per-rule nonce is a
|
|
499
|
+
// u32; values above SOROBAN_LIMITS.u32Max cannot be installed). Mirrors
|
|
500
|
+
// the validUntilLedger SOROBAN_LIMITS.u32Max check above.
|
|
501
|
+
if (nonce !== undefined && (!isPositiveInt(nonce) || nonce > SOROBAN_LIMITS.u32Max)) {
|
|
502
|
+
return synthesisError(`interpreter.installNonce must be a positive u32 integer (<= ${SOROBAN_LIMITS.u32Max}), got: ${nonce}`);
|
|
470
503
|
}
|
|
471
504
|
const op = opts.interpreter.oracleParams;
|
|
472
505
|
if (op) {
|
|
@@ -484,6 +517,12 @@ function validateOptions(opts) {
|
|
|
484
517
|
}
|
|
485
518
|
return null;
|
|
486
519
|
}
|
|
520
|
+
/** Placeholder/stub smart-account prefixes (item 5). Mirrors the
|
|
521
|
+
* `PLACEHOLDER_INTERPRETER_ADDRESS` marker the interpreter adapter uses for
|
|
522
|
+
* the interpreter-contract strkey; a real install must point at a C...
|
|
523
|
+
* contract address derivable from the on-chain account, never a
|
|
524
|
+
* fixture/LLM-seam marker. */
|
|
525
|
+
const PLACEHOLDER_SMART_ACCOUNT_PREFIX = /^(VERIFY-|PLACEHOLDER-|TODO-)/i;
|
|
487
526
|
function isPositiveInt(n) {
|
|
488
527
|
return Number.isInteger(n) && n > 0;
|
|
489
528
|
}
|
|
@@ -561,18 +600,37 @@ function buildPermitContext(tx, scope, topLevel, userResponses, predicate) {
|
|
|
561
600
|
}
|
|
562
601
|
return ctx;
|
|
563
602
|
}
|
|
564
|
-
function cloneScVal(value) {
|
|
603
|
+
function cloneScVal(value, depth = 0) {
|
|
565
604
|
// EvalContext.args is an ScVal[]; the recorded args are already ScVal-shaped
|
|
566
605
|
// (decoded by the recorder). We clone top-level shells so the harness can
|
|
567
|
-
// mutate deny cases without aliasing the recorded call.
|
|
606
|
+
// mutate deny cases without aliasing the recorded call. Recursion is
|
|
607
|
+
// bounded by MAX_SCVAL_CLONE_DEPTH so a hand-crafted nested-vec payload
|
|
608
|
+
// cannot RangeError the JS stack; the over-depth branch throws a
|
|
609
|
+
// ToolError-shaped error that the `synthesizeFromRecording` envelope
|
|
610
|
+
// (item 3) converts to `{ok:false, error}`.
|
|
568
611
|
if (value.type === 'vec') {
|
|
612
|
+
if (depth >= MAX_SCVAL_CLONE_DEPTH) {
|
|
613
|
+
throw cloneDepthError(value);
|
|
614
|
+
}
|
|
569
615
|
return {
|
|
570
616
|
type: 'vec',
|
|
571
|
-
value: value.value.map(cloneScVal),
|
|
617
|
+
value: value.value.map((v) => cloneScVal(v, depth + 1)),
|
|
572
618
|
};
|
|
573
619
|
}
|
|
574
620
|
return { ...value };
|
|
575
621
|
}
|
|
622
|
+
/** Throw a ToolError-shaped error when the clone exceeds MAX_SCVAL_CLONE_DEPTH.
|
|
623
|
+
* The shape (object with string `code`) is what the `synthesizeFromRecording`
|
|
624
|
+
* envelope detects and converts to a structured `{ok:false, error}`. */
|
|
625
|
+
function cloneDepthError(value) {
|
|
626
|
+
const err = new Error(`ScVal clone depth exceeds MAX_SCVAL_CLONE_DEPTH (${MAX_SCVAL_CLONE_DEPTH})`);
|
|
627
|
+
err.code = 'SYNTHESIS_ERROR';
|
|
628
|
+
err.severity = 'error';
|
|
629
|
+
err.retryable = false;
|
|
630
|
+
// Tag the offending value so failures are diagnosable.
|
|
631
|
+
err.depthContext = value.type;
|
|
632
|
+
throw err;
|
|
633
|
+
}
|
|
576
634
|
/** Walk every `oracle_price` leaf in the predicate and return a price map
|
|
577
635
|
* whose entries satisfy the bound so the intended call permits. The
|
|
578
636
|
* timestamp is pinned to `nowSeconds` (the recorded `fetchedAt`) so the
|
package/dist/types.d.ts
CHANGED
|
@@ -42,6 +42,23 @@ export declare const SOROBAN_LIMITS: {
|
|
|
42
42
|
* used to convert a spend window in seconds to OZ `period_ledgers`. */
|
|
43
43
|
readonly secondsPerLedger: 5;
|
|
44
44
|
};
|
|
45
|
+
/** Maximum recursion depth for decoding Recursive `ScVal` shapes (vec-in-vec).
|
|
46
|
+
* A real Stellar tx caps nesting well below this; the bound hard-caps a
|
|
47
|
+
* hand-crafted payload so the recorder fails CLOSED (records the over-depth
|
|
48
|
+
* branch as opaque `depth-exceeded`) rather than RangeErroring the JS stack.
|
|
49
|
+
* Mirrors the `MAX_AUTH_TREE_DEPTH = 16` boundary the auth-tree decoder
|
|
50
|
+
* already enforces; lifted to 30 so legitimate nested args (a path vec
|
|
51
|
+
* inside an order vec inside a market-data vec) decode cleanly while a
|
|
52
|
+
* malformed ~10000-deep payload still trips the cap. */
|
|
53
|
+
export declare const MAX_SCVAL_DEPTH: 30;
|
|
54
|
+
/** Maximum recursion depth for cloning nested `ScVal` shapes (vec-in-vec) into
|
|
55
|
+
* the simulator / verify / harness contexts. Mirrors `MAX_SCVAL_DEPTH` above
|
|
56
|
+
* so a hand-crafted nested-vec payload cannot RangeError the JS stack during
|
|
57
|
+
* context building. Over-depth throws a ToolError (caught by the
|
|
58
|
+
* `synthesizeFromRecording` envelope + the simulator/verify boundaries) so
|
|
59
|
+
* the caller gets a structured `{ok:false, error}` instead of a thrown
|
|
60
|
+
* RangeError. */
|
|
61
|
+
export declare const MAX_SCVAL_CLONE_DEPTH: 30;
|
|
45
62
|
/** Predicate-document caps - enforced fail-closed at install by BOTH the synth (TS)
|
|
46
63
|
* AND the interpreter (Rust). Single source: a CI test asserts the two cap sets are
|
|
47
64
|
* byte-identical (TS reads the same JSON manifest the Rust build emits). If they
|
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
|