@crediolabs/policy-synth 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/codegen/template.js +3 -3
- package/dist/record/decode.d.ts +6 -3
- package/dist/record/decode.js +56 -3
- package/dist/record/freshness.d.ts +14 -1
- package/dist/record/freshness.js +32 -2
- package/dist/record/index.d.ts +11 -0
- package/dist/record/index.js +25 -0
- package/dist/record/movements.d.ts +15 -3
- package/dist/record/movements.js +42 -7
- package/dist/run/index.d.ts +43 -0
- package/dist/run/index.js +222 -0
- package/dist/run/schemas.d.ts +2016 -0
- package/dist/run/schemas.js +223 -0
- package/dist/synth/address.d.ts +7 -0
- package/dist/synth/address.js +12 -0
- package/dist/synth/compose-from-recording.d.ts +4 -3
- package/dist/synth/compose-from-recording.js +16 -6
- package/dist/synth/deny-cases.d.ts +12 -2
- package/dist/synth/deny-cases.js +76 -4
- package/dist/synth/evaluate.js +2 -2
- package/dist/synth/index.d.ts +1 -0
- package/dist/synth/index.js +4 -0
- package/dist/synth/minimize.d.ts +1 -1
- package/dist/synth/minimize.js +3 -3
- package/dist/synth/synthesize-from-recording.d.ts +5 -2
- package/dist/synth/synthesize-from-recording.js +175 -69
- package/dist/types.d.ts +41 -1
- package/dist/types.js +17 -0
- package/dist/verify/simulate.js +18 -2
- package/dist/verify/verify.js +18 -2
- package/dist-cjs/adapters/interpreter/adapter.d.ts +38 -0
- package/dist-cjs/adapters/interpreter/adapter.js +527 -0
- package/dist-cjs/adapters/interpreter/index.d.ts +1 -0
- package/dist-cjs/adapters/interpreter/index.js +8 -0
- package/dist-cjs/adapters/oz/adapter.d.ts +20 -0
- package/dist-cjs/adapters/oz/adapter.js +289 -0
- package/dist-cjs/adapters/oz/index.d.ts +1 -0
- package/dist-cjs/adapters/oz/index.js +8 -0
- package/dist-cjs/codegen/compile-gate.d.ts +33 -0
- package/dist-cjs/codegen/compile-gate.js +123 -0
- package/dist-cjs/codegen/index.d.ts +2 -0
- package/dist-cjs/codegen/index.js +14 -0
- package/dist-cjs/codegen/template.d.ts +18 -0
- package/dist-cjs/codegen/template.js +134 -0
- package/dist-cjs/errors.d.ts +37 -0
- package/dist-cjs/errors.js +3 -0
- package/dist-cjs/index.d.ts +11 -0
- package/dist-cjs/index.js +27 -0
- package/dist-cjs/ir/index.d.ts +1 -0
- package/dist-cjs/ir/index.js +3 -0
- package/dist-cjs/ir/types.d.ts +108 -0
- package/dist-cjs/ir/types.js +12 -0
- package/dist-cjs/mandate/index.d.ts +2 -0
- package/dist-cjs/mandate/index.js +6 -0
- package/dist-cjs/mandate/to-ir.d.ts +3 -0
- package/dist-cjs/mandate/to-ir.js +63 -0
- package/dist-cjs/mandate/types.d.ts +20 -0
- package/dist-cjs/mandate/types.js +9 -0
- package/dist-cjs/package.json +3 -0
- package/dist-cjs/predicate/encode.d.ts +10 -0
- package/dist-cjs/predicate/encode.js +252 -0
- package/dist-cjs/predicate/index.d.ts +1 -0
- package/dist-cjs/predicate/index.js +6 -0
- package/dist-cjs/record/decode.d.ts +79 -0
- package/dist-cjs/record/decode.js +441 -0
- package/dist-cjs/record/freshness.d.ts +30 -0
- package/dist-cjs/record/freshness.js +85 -0
- package/dist-cjs/record/index.d.ts +32 -0
- package/dist-cjs/record/index.js +191 -0
- package/dist-cjs/record/movements.d.ts +32 -0
- package/dist-cjs/record/movements.js +227 -0
- package/dist-cjs/record/rpc.d.ts +22 -0
- package/dist-cjs/record/rpc.js +74 -0
- package/dist-cjs/record/validate.d.ts +22 -0
- package/dist-cjs/record/validate.js +63 -0
- package/dist-cjs/registry/identify.d.ts +11 -0
- package/dist-cjs/registry/identify.js +87 -0
- package/dist-cjs/registry/index.d.ts +3 -0
- package/dist-cjs/registry/index.js +15 -0
- package/dist-cjs/registry/known-addresses.d.ts +16 -0
- package/dist-cjs/registry/known-addresses.js +53 -0
- package/dist-cjs/registry/protocols.d.ts +38 -0
- package/dist-cjs/registry/protocols.js +153 -0
- package/dist-cjs/review-card/builder.d.ts +14 -0
- package/dist-cjs/review-card/builder.js +264 -0
- package/dist-cjs/review-card/conflict.d.ts +40 -0
- package/dist-cjs/review-card/conflict.js +114 -0
- package/dist-cjs/review-card/cross-check.d.ts +11 -0
- package/dist-cjs/review-card/cross-check.js +151 -0
- package/dist-cjs/review-card/index.d.ts +3 -0
- package/dist-cjs/review-card/index.js +10 -0
- package/dist-cjs/run/index.d.ts +43 -0
- package/dist-cjs/run/index.js +237 -0
- package/dist-cjs/run/schemas.d.ts +2016 -0
- package/dist-cjs/run/schemas.js +226 -0
- package/dist-cjs/seams/index.d.ts +1 -0
- package/dist-cjs/seams/index.js +3 -0
- package/dist-cjs/seams/types.d.ts +66 -0
- package/dist-cjs/seams/types.js +12 -0
- package/dist-cjs/synth/address.d.ts +7 -0
- package/dist-cjs/synth/address.js +15 -0
- package/dist-cjs/synth/compose-from-recording.d.ts +65 -0
- package/dist-cjs/synth/compose-from-recording.js +366 -0
- package/dist-cjs/synth/deny-cases.d.ts +22 -0
- package/dist-cjs/synth/deny-cases.js +438 -0
- package/dist-cjs/synth/evaluate.d.ts +39 -0
- package/dist-cjs/synth/evaluate.js +428 -0
- package/dist-cjs/synth/harness.d.ts +16 -0
- package/dist-cjs/synth/harness.js +29 -0
- package/dist-cjs/synth/index.d.ts +10 -0
- package/dist-cjs/synth/index.js +28 -0
- package/dist-cjs/synth/lower.d.ts +23 -0
- package/dist-cjs/synth/lower.js +119 -0
- package/dist-cjs/synth/minimize.d.ts +4 -0
- package/dist-cjs/synth/minimize.js +41 -0
- package/dist-cjs/synth/predicate-literals.d.ts +5 -0
- package/dist-cjs/synth/predicate-literals.js +28 -0
- package/dist-cjs/synth/scope.d.ts +26 -0
- package/dist-cjs/synth/scope.js +81 -0
- package/dist-cjs/synth/synthesize-from-mandate.d.ts +5 -0
- package/dist-cjs/synth/synthesize-from-mandate.js +37 -0
- package/dist-cjs/synth/synthesize-from-recording.d.ts +49 -0
- package/dist-cjs/synth/synthesize-from-recording.js +759 -0
- package/dist-cjs/types.d.ts +311 -0
- package/dist-cjs/types.js +55 -0
- package/dist-cjs/verify/envelope.d.ts +15 -0
- package/dist-cjs/verify/envelope.js +23 -0
- package/dist-cjs/verify/index.d.ts +3 -0
- package/dist-cjs/verify/index.js +8 -0
- package/dist-cjs/verify/simulate.d.ts +31 -0
- package/dist-cjs/verify/simulate.js +261 -0
- package/dist-cjs/verify/verify.d.ts +21 -0
- package/dist-cjs/verify/verify.js +192 -0
- package/package.json +43 -3
- package/src/codegen/template.ts +3 -3
- package/src/contracts/policy-template/OZ_POLICY_TRAIT.md +171 -0
- package/src/record/corpus-fixtures.json +532 -0
- package/src/record/decode.ts +63 -10
- package/src/record/freshness.ts +34 -2
- package/src/record/index.ts +40 -0
- package/src/record/movements.ts +40 -7
- package/src/run/index.ts +277 -0
- package/src/run/schemas.ts +250 -0
- package/src/synth/address.ts +14 -0
- package/src/synth/compose-from-recording.ts +20 -9
- package/src/synth/deny-cases.ts +87 -4
- package/src/synth/evaluate.ts +2 -2
- package/src/synth/index.ts +4 -0
- package/src/synth/minimize.ts +7 -3
- package/src/synth/synthesize-from-recording.ts +200 -68
- package/src/types.ts +37 -1
- package/src/verify/simulate.ts +21 -2
- package/src/verify/verify.ts +21 -2
|
@@ -0,0 +1,223 @@
|
|
|
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
|
+
import { isStellarAddress } from "../synth/address.js";
|
|
19
|
+
/** Soroban `valid_until` is a u32 ledger sequence; a value above this cannot be
|
|
20
|
+
* installed on-chain, so reject it at the boundary (fail-closed). */
|
|
21
|
+
const U32_MAX = 4294967295;
|
|
22
|
+
/** Upper bound on top-level invocations in a recorded transaction. A real
|
|
23
|
+
* Stellar tx caps operations well below this; the bound stops a hand-crafted
|
|
24
|
+
* payload from turning one request into an unbounded synthesis (DoS). */
|
|
25
|
+
const MAX_INVOCATIONS = 512;
|
|
26
|
+
export const NetworkSchema = z.enum(['mainnet', 'testnet']);
|
|
27
|
+
/** ScVal subset - normalised subset the synth consumes. Mirrors
|
|
28
|
+
* `ScVal` in packages/policy-synth/src/types.ts. */
|
|
29
|
+
export const ScValSchema = z.lazy(() => z.union([
|
|
30
|
+
z.object({ type: z.literal('address'), value: z.string() }),
|
|
31
|
+
// i128 is SIGNED: real events carry negatives (e.g. a fee-adjustment/refund),
|
|
32
|
+
// so the recorder's own output must round-trip through this schema. u64/u32
|
|
33
|
+
// are unsigned and stay non-negative.
|
|
34
|
+
z.object({ type: z.literal('i128'), value: z.string().regex(/^-?[0-9]+$/) }),
|
|
35
|
+
z.object({ type: z.literal('u64'), value: z.string().regex(/^[0-9]+$/) }),
|
|
36
|
+
z.object({ type: z.literal('u32'), value: z.string().regex(/^[0-9]+$/) }),
|
|
37
|
+
z.object({ type: z.literal('symbol'), value: z.string() }),
|
|
38
|
+
z.object({ type: z.literal('vec'), value: z.array(ScValSchema) }),
|
|
39
|
+
z.object({ type: z.literal('bytes'), value: z.string() }),
|
|
40
|
+
z.object({ type: z.literal('other'), value: z.string() }),
|
|
41
|
+
]));
|
|
42
|
+
/** ContractInvocation mirrors the core. Annotated with an explicit
|
|
43
|
+
* `z.ZodType<unknown>` (like ScValSchema above) so the self-referential
|
|
44
|
+
* `subInvocations` field does not trip TS's circular type inference. */
|
|
45
|
+
export const ContractInvocationSchema = z.object({
|
|
46
|
+
contract: z.string(),
|
|
47
|
+
fn: z.string(),
|
|
48
|
+
args: z.array(ScValSchema),
|
|
49
|
+
subInvocations: z.array(z.lazy(() => ContractInvocationSchema)),
|
|
50
|
+
});
|
|
51
|
+
export const TokenMovementSchema = z.object({
|
|
52
|
+
token: z.string(),
|
|
53
|
+
from: z.string(),
|
|
54
|
+
to: z.string(),
|
|
55
|
+
// The recorder reads the amount straight from the signed i128 event value
|
|
56
|
+
// (record/movements.ts readAmount), so a non-standard token that emits a
|
|
57
|
+
// negative transfer/mint/burn amount round-trips as a negative string. Mirror
|
|
58
|
+
// that here; the synth gate, not the wire schema, decides what to do with it.
|
|
59
|
+
amount: z.string().regex(/^-?[0-9]+$/),
|
|
60
|
+
});
|
|
61
|
+
export const OnChainEventSchema = z.object({
|
|
62
|
+
contract: z.string(),
|
|
63
|
+
topics: z.array(z.string()),
|
|
64
|
+
data: ScValSchema,
|
|
65
|
+
});
|
|
66
|
+
export const ParseConfidenceSchema = z.object({
|
|
67
|
+
overall: z.number().min(0).max(1),
|
|
68
|
+
knownContracts: z.array(z.string()),
|
|
69
|
+
unknownContracts: z.array(z.object({
|
|
70
|
+
contract: z.string(),
|
|
71
|
+
reason: z.enum(['no-abi', 'version-mismatch', 'opaque-result']),
|
|
72
|
+
})),
|
|
73
|
+
opaqueScVals: z.array(z.object({ path: z.string(), type: z.string() })),
|
|
74
|
+
thresholdUsed: z.number().min(0).max(1),
|
|
75
|
+
});
|
|
76
|
+
/** RecordedTransaction mirrors the core RecordedTransaction. The output shape
|
|
77
|
+
* is referenced by name in the tool result structured content; we deliberately
|
|
78
|
+
* type it loosely (`z.unknown()`) on the success path so the core remains the
|
|
79
|
+
* single source of truth for the wire payload. */
|
|
80
|
+
export const RecordedTransactionSchema = z
|
|
81
|
+
.object({
|
|
82
|
+
network: NetworkSchema,
|
|
83
|
+
signers: z.array(z.string()),
|
|
84
|
+
invocations: z.array(ContractInvocationSchema).max(MAX_INVOCATIONS),
|
|
85
|
+
tokenMovements: z.array(TokenMovementSchema),
|
|
86
|
+
events: z.array(OnChainEventSchema),
|
|
87
|
+
authEntries: z.array(z.unknown()),
|
|
88
|
+
ledgerSequence: z.number().int().nonnegative(),
|
|
89
|
+
fetchedAt: z.number().int().nonnegative(),
|
|
90
|
+
parseConfidence: ParseConfidenceSchema,
|
|
91
|
+
sourceAccount: z.string(),
|
|
92
|
+
})
|
|
93
|
+
.passthrough();
|
|
94
|
+
/** MandateSpec mirrors the core MandateSpec. The deterministic Mandate
|
|
95
|
+
* front-end needs no parseConfidence; the tool adapter injects the full
|
|
96
|
+
* confidence after synthesis so the orchestrator can compare. */
|
|
97
|
+
export const MandateSpecSchema = z
|
|
98
|
+
.object({
|
|
99
|
+
chain: z.literal('stellar'),
|
|
100
|
+
contract: z.string(),
|
|
101
|
+
method: z.string().optional(),
|
|
102
|
+
spendingLimit: z
|
|
103
|
+
.object({
|
|
104
|
+
token: z.string(),
|
|
105
|
+
limit: z.string().regex(/^[0-9]+$/),
|
|
106
|
+
windowSeconds: z.number().int().positive(),
|
|
107
|
+
})
|
|
108
|
+
.optional(),
|
|
109
|
+
// A threshold of 0 means "0 approvals", which is not a real M-of-N gate.
|
|
110
|
+
approvalThreshold: z.number().int().positive().optional(),
|
|
111
|
+
recipients: z.array(z.string()).optional(),
|
|
112
|
+
expiry: z
|
|
113
|
+
.object({
|
|
114
|
+
validUntilLedger: z.number().int().positive().max(U32_MAX).optional(),
|
|
115
|
+
validUntilUnixSeconds: z.number().int().positive().optional(),
|
|
116
|
+
})
|
|
117
|
+
.optional(),
|
|
118
|
+
})
|
|
119
|
+
.passthrough();
|
|
120
|
+
/** ComposeUserResponses mirrors the core. */
|
|
121
|
+
export const ComposeUserResponsesSchema = z
|
|
122
|
+
.object({
|
|
123
|
+
windowSeconds: z.number().int().positive().optional(),
|
|
124
|
+
validUntilLedger: z.number().int().positive().max(U32_MAX).optional(),
|
|
125
|
+
limitAmount: z
|
|
126
|
+
.string()
|
|
127
|
+
.regex(/^[0-9]+$/)
|
|
128
|
+
.optional(),
|
|
129
|
+
invocationLimit: z.number().int().positive().optional(),
|
|
130
|
+
// Swap recipient allowlist (SoroSwap call_arg[3]). Each entry must be a
|
|
131
|
+
// Stellar address (G... wallet or C... contract); supplying it REPLACES the
|
|
132
|
+
// default pin to the recorded recipient. Validated with the shared StrKey
|
|
133
|
+
// helper (no hand-rolled regex).
|
|
134
|
+
swapRecipientAllowlist: z
|
|
135
|
+
.array(z.string().refine(isStellarAddress, 'must be a Stellar address (G... or C...)'))
|
|
136
|
+
.optional(),
|
|
137
|
+
})
|
|
138
|
+
.passthrough();
|
|
139
|
+
/** OzAdapterConfig - the per-network OZ built-in instance addresses. */
|
|
140
|
+
export const OzAdapterConfigSchema = z.object({
|
|
141
|
+
network: NetworkSchema,
|
|
142
|
+
instances: z.object({
|
|
143
|
+
spending_limit: z.string(),
|
|
144
|
+
simple_threshold: z.string(),
|
|
145
|
+
weighted_threshold: z.string(),
|
|
146
|
+
}),
|
|
147
|
+
});
|
|
148
|
+
// ===== record_transaction =====
|
|
149
|
+
export const RecordTransactionInputSchema = z
|
|
150
|
+
.object({
|
|
151
|
+
hash: z.string().min(1).optional(),
|
|
152
|
+
xdr: z.string().min(1).optional(),
|
|
153
|
+
network: NetworkSchema,
|
|
154
|
+
confidenceOverride: z.number().min(0).max(1).optional(),
|
|
155
|
+
})
|
|
156
|
+
.refine((v) => !(v.hash && v.xdr), {
|
|
157
|
+
message: 'provide exactly one of `hash` or `xdr`, not both',
|
|
158
|
+
})
|
|
159
|
+
.refine((v) => Boolean(v.hash) || Boolean(v.xdr), {
|
|
160
|
+
message: 'one of `hash` or `xdr` is required',
|
|
161
|
+
});
|
|
162
|
+
// ===== synthesize_policy =====
|
|
163
|
+
//
|
|
164
|
+
// Discriminated union on `source` exposes BOTH front-ends through ONE tool.
|
|
165
|
+
// - `source: 'mandate'` -> calls synthesizeFromMandate
|
|
166
|
+
// - `source: 'recording'` -> calls synthesizeFromRecording
|
|
167
|
+
export const SynthesizePolicyMandateInputSchema = z.object({
|
|
168
|
+
source: z.literal('mandate'),
|
|
169
|
+
mandate: MandateSpecSchema,
|
|
170
|
+
ozConfig: OzAdapterConfigSchema.optional(),
|
|
171
|
+
});
|
|
172
|
+
/** Interpreter opt-in for the recording path. Present -> constraints OZ cannot
|
|
173
|
+
* express (per-method scoping, invocation-count windows, oracle bounds, exact
|
|
174
|
+
* hop paths) lower to a real interpreter predicate document instead of being
|
|
175
|
+
* surfaced as warnings. The core deep-validates `smartAccountAddress` (a C...
|
|
176
|
+
* contract, not the recording's G... source) and the tighten-only oracle
|
|
177
|
+
* bounds; the schema stays light so the core owns the friendly ToolErrors. */
|
|
178
|
+
export const InterpreterOptionsSchema = z.object({
|
|
179
|
+
smartAccountAddress: z.string(),
|
|
180
|
+
installNonce: z.number().int().positive().optional(),
|
|
181
|
+
oracleParams: z
|
|
182
|
+
.object({
|
|
183
|
+
maxStalenessSeconds: z.number().int().positive().optional(),
|
|
184
|
+
maxDeviationBps: z.number().int().positive().optional(),
|
|
185
|
+
})
|
|
186
|
+
.optional(),
|
|
187
|
+
});
|
|
188
|
+
export const SynthesizePolicyRecordingInputSchema = z.object({
|
|
189
|
+
source: z.literal('recording'),
|
|
190
|
+
recordedTx: RecordedTransactionSchema,
|
|
191
|
+
network: NetworkSchema,
|
|
192
|
+
userResponses: ComposeUserResponsesSchema.optional(),
|
|
193
|
+
confidenceOverride: z.object({ threshold: z.number().min(0).max(1) }).optional(),
|
|
194
|
+
interpreter: InterpreterOptionsSchema.optional(),
|
|
195
|
+
ozConfig: OzAdapterConfigSchema.optional(),
|
|
196
|
+
});
|
|
197
|
+
export const SynthesizePolicyInputSchema = z.discriminatedUnion('source', [
|
|
198
|
+
SynthesizePolicyMandateInputSchema,
|
|
199
|
+
SynthesizePolicyRecordingInputSchema,
|
|
200
|
+
]);
|
|
201
|
+
// ===== Error envelope (canonical) =====
|
|
202
|
+
//
|
|
203
|
+
// Mirrors ToolError from packages/policy-synth/src/errors.ts. We use a
|
|
204
|
+
// `z.string()` for `code` (not an enum) because the core's ErrorCode union
|
|
205
|
+
// evolves over time; the transport contract only promises a string code the
|
|
206
|
+
// caller can dispatch on. A drift test asserts the canonical codes still
|
|
207
|
+
// pass through unchanged.
|
|
208
|
+
export const ToolErrorSchema = z
|
|
209
|
+
.object({
|
|
210
|
+
code: z.string(),
|
|
211
|
+
message: z.string(),
|
|
212
|
+
severity: z.enum(['info', 'warning', 'error', 'fatal']),
|
|
213
|
+
retryable: z.boolean(),
|
|
214
|
+
remediation: z
|
|
215
|
+
.object({
|
|
216
|
+
toolCall: z.object({ name: z.string(), args: z.record(z.unknown()) }).optional(),
|
|
217
|
+
userQuestion: z.object({ code: z.string(), question: z.string() }).optional(),
|
|
218
|
+
docsUrl: z.string().optional(),
|
|
219
|
+
})
|
|
220
|
+
.optional(),
|
|
221
|
+
details: z.unknown().optional(),
|
|
222
|
+
})
|
|
223
|
+
.passthrough();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** True when `s` is a valid Stellar address strkey - either an Ed25519 public
|
|
2
|
+
* key (`G...`) or a contract address (`C...`). Backed by the SDK's `StrKey`
|
|
3
|
+
* decoder (the same one the recorder uses in `record/decode.ts`); no
|
|
4
|
+
* hand-rolled regex. Used to validate a caller-supplied swap recipient
|
|
5
|
+
* allowlist, whose entries may be either a wallet (`G...`) or a contract
|
|
6
|
+
* (`C...`). */
|
|
7
|
+
export declare function isStellarAddress(s: string): boolean;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// src/synth/address.ts - Stellar address validation shared by the CLI + the
|
|
2
|
+
// run-layer Zod schema.
|
|
3
|
+
import { StrKey } from '@stellar/stellar-sdk';
|
|
4
|
+
/** True when `s` is a valid Stellar address strkey - either an Ed25519 public
|
|
5
|
+
* key (`G...`) or a contract address (`C...`). Backed by the SDK's `StrKey`
|
|
6
|
+
* decoder (the same one the recorder uses in `record/decode.ts`); no
|
|
7
|
+
* hand-rolled regex. Used to validate a caller-supplied swap recipient
|
|
8
|
+
* allowlist, whose entries may be either a wallet (`G...`) or a contract
|
|
9
|
+
* (`C...`). */
|
|
10
|
+
export function isStellarAddress(s) {
|
|
11
|
+
return StrKey.isValidEd25519PublicKey(s) || StrKey.isValidContract(s);
|
|
12
|
+
}
|
|
@@ -28,9 +28,10 @@ export interface ComposeUserResponses {
|
|
|
28
28
|
* entries on the same asset emit multiple leaves. */
|
|
29
29
|
oraclePriceBound?: OraclePriceBound[];
|
|
30
30
|
/** Recipient allowlist for a swap (call_arg[3] on SoroSwap's
|
|
31
|
-
* swap_exact_tokens_for_tokens).
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* swap_exact_tokens_for_tokens). When supplied, it REPLACES the default
|
|
32
|
+
* pin. Absent -> the recipient is pinned to the recorded value (mirroring
|
|
33
|
+
* SEP-41) and RECIPIENT_ALLOWLIST_EMPTY is surfaced as informational, never
|
|
34
|
+
* a silent free pass. */
|
|
34
35
|
swapRecipientAllowlist?: string[];
|
|
35
36
|
}
|
|
36
37
|
/** Composition options. */
|
|
@@ -308,11 +308,16 @@ function appendProtocolSpecificConstraints(ozConstraints, interpreterConstraints
|
|
|
308
308
|
}
|
|
309
309
|
// Swap recipient (call_arg[3]): when the caller supplies
|
|
310
310
|
// swapRecipientAllowlist, emit it as an `in` constraint on the recipient
|
|
311
|
-
// arg. When absent,
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
311
|
+
// arg. When absent, PIN the recipient to the recorded value - mirroring the
|
|
312
|
+
// SEP-41 `to` arg above: the recorded flow went to exactly one recipient, so
|
|
313
|
+
// pinning it is the minimal policy that permits exactly that flow. Leaving
|
|
314
|
+
// it unconstrained would permit an arbitrary recipient (an evil twin with
|
|
315
|
+
// call_arg[3] = attacker_wallet). RECIPIENT_ALLOWLIST_EMPTY is still
|
|
316
|
+
// surfaced, but as INFORMATIONAL (the recipient was pinned; here is how to
|
|
317
|
+
// widen it), never as a silent free pass. The recipient is only enforceable
|
|
318
|
+
// via the interpreter predicate, so the pin (and the ambiguity) apply only
|
|
319
|
+
// when the interpreter is enabled - otherwise the swap recipient is ignored
|
|
320
|
+
// (today's behaviour, matching the other SoroSwap constraints).
|
|
316
321
|
const recipientArg = topLevel.args[3];
|
|
317
322
|
if (swapRecipientAllowlist && swapRecipientAllowlist.length > 0) {
|
|
318
323
|
const cond = {
|
|
@@ -328,9 +333,14 @@ function appendProtocolSpecificConstraints(ozConstraints, interpreterConstraints
|
|
|
328
333
|
}
|
|
329
334
|
}
|
|
330
335
|
else if (interpreterEnabled && recipientArg && recipientArg.type === 'address') {
|
|
336
|
+
interpreterConstraints.push({
|
|
337
|
+
op: 'in',
|
|
338
|
+
selector: { kind: 'arg', argIndex: 3, scalarType: 'address' },
|
|
339
|
+
values: [recipientArg.value],
|
|
340
|
+
});
|
|
331
341
|
ambiguities.push({
|
|
332
342
|
code: 'RECIPIENT_ALLOWLIST_EMPTY',
|
|
333
|
-
question: `
|
|
343
|
+
question: `No recipient allowlist supplied; the swap recipient (call_arg[3]) was pinned to the recorded value ${recipientArg.value}. To permit additional recipients, supply an allowlist (CLI: --recipient <C...|G...>, repeatable; MCP: userResponses.swapRecipientAllowlist) - it REPLACES this default pin.`,
|
|
334
344
|
});
|
|
335
345
|
}
|
|
336
346
|
}
|
|
@@ -8,5 +8,15 @@ export interface GeneratedCases {
|
|
|
8
8
|
permit: EvalContext;
|
|
9
9
|
denies: DenyCase[];
|
|
10
10
|
}
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
declare const OVERPERMISSIVE_DIMENSIONS: readonly ["argument_reorder"];
|
|
12
|
+
declare const ORIGINAL_DIMENSIONS: string[];
|
|
13
|
+
export { ORIGINAL_DIMENSIONS, OVERPERMISSIVE_DIMENSIONS };
|
|
14
|
+
/** Build deterministic model-evaluated alternatives without mutating the intended call.
|
|
15
|
+
*
|
|
16
|
+
* @param predicate - the synthesized predicate
|
|
17
|
+
* @param permitCtx - EvalContext for the intended (permitted) call
|
|
18
|
+
* @param dimensions - optional whitelist of dimension names to emit; when omitted
|
|
19
|
+
* all known dimensions (including over-permissiveness mutations)
|
|
20
|
+
* are emitted. The synth pipeline passes ORIGINAL_DIMENSIONS so
|
|
21
|
+
* existing fixtures do not regress. */
|
|
22
|
+
export declare function generateCases(predicate: PredicateNode, permitCtx: EvalContext, dimensions?: string[]): GeneratedCases;
|
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'],
|
|
@@ -10,8 +11,38 @@ const ADJACENT_ASSETS = [
|
|
|
10
11
|
'CAS3J7GYLGXMF6TDJ5WQ2PEN4GRVNXJUIQ2TZU3ZB3OQ2V4DRCWI7WPF',
|
|
11
12
|
'CCWCLTASNDT57N3BCHOSVB5QWMV5URK4BXLDDF6ZZQYMBQ4OKZA3ZB2N',
|
|
12
13
|
];
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
// Phase 1 property-harness mutation dimensions excluded from the synth pipeline's
|
|
15
|
+
// self-verify call so existing fixtures still emit policies. The harness tests
|
|
16
|
+
// them as FINDINGS against the already-emitted policy.
|
|
17
|
+
const OVERPERMISSIVE_DIMENSIONS = ['argument_reorder'];
|
|
18
|
+
// The 15 dimensions the synth pipeline uses for self-verify and minimise.
|
|
19
|
+
const ORIGINAL_DIMENSIONS = [
|
|
20
|
+
'amount',
|
|
21
|
+
'asset',
|
|
22
|
+
'contract',
|
|
23
|
+
'function',
|
|
24
|
+
'timing',
|
|
25
|
+
'time_window',
|
|
26
|
+
'invocation_count',
|
|
27
|
+
'arg_amount_bound',
|
|
28
|
+
'arg_bound',
|
|
29
|
+
'scope_contract_fn_arg',
|
|
30
|
+
'oracle_stale',
|
|
31
|
+
'oracle_missing',
|
|
32
|
+
'oracle_deviation_exceeded',
|
|
33
|
+
'oracle_paused',
|
|
34
|
+
'soroswap_allowed_path',
|
|
35
|
+
];
|
|
36
|
+
export { ORIGINAL_DIMENSIONS, OVERPERMISSIVE_DIMENSIONS };
|
|
37
|
+
/** Build deterministic model-evaluated alternatives without mutating the intended call.
|
|
38
|
+
*
|
|
39
|
+
* @param predicate - the synthesized predicate
|
|
40
|
+
* @param permitCtx - EvalContext for the intended (permitted) call
|
|
41
|
+
* @param dimensions - optional whitelist of dimension names to emit; when omitted
|
|
42
|
+
* all known dimensions (including over-permissiveness mutations)
|
|
43
|
+
* are emitted. The synth pipeline passes ORIGINAL_DIMENSIONS so
|
|
44
|
+
* existing fixtures do not regress. */
|
|
45
|
+
export function generateCases(predicate, permitCtx, dimensions) {
|
|
15
46
|
const facts = inspectPredicate(predicate);
|
|
16
47
|
const denies = [];
|
|
17
48
|
for (const comparison of facts.comparisons) {
|
|
@@ -143,6 +174,32 @@ export function generateCases(predicate, permitCtx) {
|
|
|
143
174
|
ctx.args[comparison.left.index] = differentVector(ctx.args[comparison.left.index]);
|
|
144
175
|
denies.push({ dimension: 'soroswap_allowed_path', ctx });
|
|
145
176
|
}
|
|
177
|
+
// --- argument_reorder: swap first two address args ---
|
|
178
|
+
// Skipped when dimensions filter is active so the synth pipeline can emit a policy;
|
|
179
|
+
// the over-permissiveness harness then tests this mutation as a FINDING.
|
|
180
|
+
const constrainedArgIndices = new Set();
|
|
181
|
+
for (const comparison of facts.comparisons) {
|
|
182
|
+
if (comparison.left.kind === 'call_arg')
|
|
183
|
+
constrainedArgIndices.add(comparison.left.index);
|
|
184
|
+
}
|
|
185
|
+
for (const membership of facts.memberships) {
|
|
186
|
+
if (membership.needle.kind === 'call_arg')
|
|
187
|
+
constrainedArgIndices.add(membership.needle.index);
|
|
188
|
+
}
|
|
189
|
+
const hasConstrainedArg = constrainedArgIndices.size > 0;
|
|
190
|
+
if ((!dimensions || dimensions.includes('argument_reorder')) &&
|
|
191
|
+
hasConstrainedArg &&
|
|
192
|
+
permitCtx.args.length >= 2 &&
|
|
193
|
+
permitCtx.args[0]?.type === 'address' &&
|
|
194
|
+
permitCtx.args[1]?.type === 'address') {
|
|
195
|
+
const ctx = cloneContext(permitCtx);
|
|
196
|
+
// Guard above guarantees args[0] and args[1] exist and are address-typed.
|
|
197
|
+
const a0 = ctx.args[0];
|
|
198
|
+
const a1 = ctx.args[1];
|
|
199
|
+
ctx.args[0] = a1;
|
|
200
|
+
ctx.args[1] = a0;
|
|
201
|
+
denies.push({ dimension: 'argument_reorder', ctx });
|
|
202
|
+
}
|
|
146
203
|
// Version mismatch, malformed predicates, master authorization, and nonce replay are install-time checks and are intentionally omitted from model-evaluated cases.
|
|
147
204
|
return { permit: cloneContext(permitCtx), denies };
|
|
148
205
|
}
|
|
@@ -353,9 +410,24 @@ function cloneContext(ctx) {
|
|
|
353
410
|
cloned.signerWeights = { ...ctx.signerWeights };
|
|
354
411
|
return cloned;
|
|
355
412
|
}
|
|
356
|
-
function cloneScVal(value) {
|
|
413
|
+
function cloneScVal(value, depth = 0) {
|
|
414
|
+
// Recursion is bounded by MAX_SCVAL_CLONE_DEPTH so a hand-crafted nested-vec
|
|
415
|
+
// payload cannot RangeError the JS stack during deny-case mutation. Over-depth
|
|
416
|
+
// throws a ToolError-shaped error that the `synthesizeFromRecording` envelope
|
|
417
|
+
// (item 3) converts to a structured `{ok:false, error}`.
|
|
357
418
|
if (value.type === 'vec') {
|
|
358
|
-
|
|
419
|
+
if (depth >= MAX_SCVAL_CLONE_DEPTH) {
|
|
420
|
+
throw cloneDepthError(value);
|
|
421
|
+
}
|
|
422
|
+
return { type: 'vec', value: value.value.map((v) => cloneScVal(v, depth + 1)) };
|
|
359
423
|
}
|
|
360
424
|
return { ...value };
|
|
361
425
|
}
|
|
426
|
+
function cloneDepthError(value) {
|
|
427
|
+
const err = new Error(`ScVal clone depth exceeds MAX_SCVAL_CLONE_DEPTH (${MAX_SCVAL_CLONE_DEPTH})`);
|
|
428
|
+
err.code = 'SYNTHESIS_ERROR';
|
|
429
|
+
err.severity = 'error';
|
|
430
|
+
err.retryable = false;
|
|
431
|
+
err.depthContext = value.type;
|
|
432
|
+
throw err;
|
|
433
|
+
}
|
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 }
|
package/dist/synth/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { isStellarAddress } from './address.ts';
|
|
1
2
|
export { type ComposeOptions, type ComposeResult, composeFromRecording, } from './compose-from-recording.ts';
|
|
2
3
|
export { type DenyCase, type GeneratedCases, generateCases, } from './deny-cases.ts';
|
|
3
4
|
export { type EvalContext, type EvalResult, evaluate } from './evaluate.ts';
|
package/dist/synth/index.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
// src/synth/index.ts - re-export the synthesizer front-ends.
|
|
2
|
+
// Exported because `swapRecipientAllowlist` is part of the public synthesis
|
|
3
|
+
// input: a caller assembling one needs the same validator the schema applies,
|
|
4
|
+
// and re-deriving it elsewhere means a second address check that can drift.
|
|
5
|
+
export { isStellarAddress } from "./address.js";
|
|
2
6
|
export { composeFromRecording, } from "./compose-from-recording.js";
|
|
3
7
|
export { generateCases, } from "./deny-cases.js";
|
|
4
8
|
export { evaluate } from "./evaluate.js";
|
package/dist/synth/minimize.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { PredicateNode } from '../types.ts';
|
|
2
2
|
import type { EvalContext } from './evaluate.ts';
|
|
3
3
|
/** Remove top-level conjuncts only when the current and regenerated batteries still deny. */
|
|
4
|
-
export declare function minimize(predicate: PredicateNode, permitCtx: EvalContext): PredicateNode;
|
|
4
|
+
export declare function minimize(predicate: PredicateNode, permitCtx: EvalContext, dimensions?: string[]): PredicateNode;
|
package/dist/synth/minimize.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { generateCases } from "./deny-cases.js";
|
|
2
2
|
import { runHarness } from "./harness.js";
|
|
3
3
|
/** Remove top-level conjuncts only when the current and regenerated batteries still deny. */
|
|
4
|
-
export function minimize(predicate, permitCtx) {
|
|
4
|
+
export function minimize(predicate, permitCtx, dimensions) {
|
|
5
5
|
if (predicate.op !== 'and')
|
|
6
6
|
return predicate;
|
|
7
7
|
let children = [...predicate.children];
|
|
8
8
|
let index = 0;
|
|
9
9
|
while (index < children.length) {
|
|
10
10
|
const current = { op: 'and', children };
|
|
11
|
-
const currentCases = generateCases(current, permitCtx);
|
|
11
|
+
const currentCases = generateCases(current, permitCtx, dimensions);
|
|
12
12
|
const candidateChildren = children.filter((_, childIndex) => childIndex !== index);
|
|
13
13
|
const candidate = { op: 'and', children: candidateChildren };
|
|
14
|
-
const candidateCases = generateCases(candidate, permitCtx);
|
|
14
|
+
const candidateCases = generateCases(candidate, permitCtx, dimensions);
|
|
15
15
|
const verificationCases = {
|
|
16
16
|
permit: candidateCases.permit,
|
|
17
17
|
denies: mergeDenyCases(currentCases.denies, candidateCases.denies),
|
|
@@ -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 };
|