@crediolabs/policy-synth 0.1.5 → 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/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 +8 -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 +8 -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 +8 -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
package/dist/record/decode.js
CHANGED
|
@@ -268,6 +268,26 @@ export function scValToSubset(val, path, opaqueScVals, depth = 0) {
|
|
|
268
268
|
value: arr.map((v, i) => scValToSubset(v, `${path}[${i}]`, opaqueScVals, depth + 1)),
|
|
269
269
|
};
|
|
270
270
|
}
|
|
271
|
+
case 'scvMap': {
|
|
272
|
+
if (depth >= MAX_SCVAL_DEPTH) {
|
|
273
|
+
opaqueScVals.push({ path, type: 'depth-exceeded' });
|
|
274
|
+
return { type: 'other', value: 'depth-exceeded' };
|
|
275
|
+
}
|
|
276
|
+
// Map<Symbol, ScVal> is the canonical data shape for SAC/SEP-41
|
|
277
|
+
// transfer/mint/burn events on Stellar mainnet (verified empirically
|
|
278
|
+
// against txs 112d2392..., eb39f493..., 50d36f5c...). The entry key is
|
|
279
|
+
// carried forward so readAmount can route to the conventional field
|
|
280
|
+
// name without re-decoding the full XDR. Non-symbol / non-string keys
|
|
281
|
+
// are stringified via the same key-to-string rule used for topics.
|
|
282
|
+
const entries = (val.map() ?? []);
|
|
283
|
+
return {
|
|
284
|
+
type: 'map',
|
|
285
|
+
value: entries.map((entry, i) => ({
|
|
286
|
+
key: mapKeyToString(entry.key(), `${path}.key[${i}]`, opaqueScVals),
|
|
287
|
+
val: scValToSubset(entry.val(), `${path}.val[${i}]`, opaqueScVals, depth + 1),
|
|
288
|
+
})),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
271
291
|
case 'scvBytes': {
|
|
272
292
|
return { type: 'bytes', value: Buffer.from(val.bytes()).toString('hex') };
|
|
273
293
|
}
|
|
@@ -345,6 +365,29 @@ export function scValToTopicString(val) {
|
|
|
345
365
|
return Buffer.from(val.bytes()).toString('hex');
|
|
346
366
|
return `<${kind}>`;
|
|
347
367
|
}
|
|
368
|
+
/** Convert a Map<Symbol, ...> key ScVal to the string used in the normalised
|
|
369
|
+
* ScVal map representation. Uses the same string-form rule as
|
|
370
|
+
* `scValToTopicString` so SAC/SEP-41 event Map keys ("amount", "to_muxed_id")
|
|
371
|
+
* resolve to the same string in both the topic path and the map-entry path.
|
|
372
|
+
* Non-symbol / non-string keys record an opaque diagnostic and return a
|
|
373
|
+
* placeholder so the Map entry is still carried forward. */
|
|
374
|
+
function mapKeyToString(val, path, opaqueScVals) {
|
|
375
|
+
const kind = val.switch().name;
|
|
376
|
+
if (kind === 'scvSymbol')
|
|
377
|
+
return val.sym().toString();
|
|
378
|
+
if (kind === 'scvString')
|
|
379
|
+
return val.str().toString();
|
|
380
|
+
if (kind === 'scvU64')
|
|
381
|
+
return u64PartsToBigInt(val.u64()).toString();
|
|
382
|
+
if (kind === 'scvU32')
|
|
383
|
+
return val.u32().toString();
|
|
384
|
+
if (kind === 'scvI128')
|
|
385
|
+
return i128PartsToBigInt(val.i128()).toString();
|
|
386
|
+
if (kind === 'scvBytes')
|
|
387
|
+
return Buffer.from(val.bytes()).toString('hex');
|
|
388
|
+
opaqueScVals.push({ path, type: `unsupported-map-key:${kind}` });
|
|
389
|
+
return `<${kind}>`;
|
|
390
|
+
}
|
|
348
391
|
/** Build the OnChainEvent[] view from raw contract events. Diagnostic-only:
|
|
349
392
|
* captures the topics + data + emitter contract for the downstream
|
|
350
393
|
* validator. */
|
|
@@ -13,5 +13,18 @@ export declare function computeParseConfidence(input: FreshnessInput): ParseConf
|
|
|
13
13
|
* ToolError with the ParseConfidence payload attached as `details`. */
|
|
14
14
|
export declare function isBelowThreshold(c: ParseConfidence): boolean;
|
|
15
15
|
/** Convenience: build the user-facing remediation question for an
|
|
16
|
-
* under-confidence recording.
|
|
16
|
+
* under-confidence recording.
|
|
17
|
+
*
|
|
18
|
+
* The remediation text branches on which diagnostic bucket is non-empty:
|
|
19
|
+
* - unknownContracts only -> user MUST supply an ABI (or re-capture
|
|
20
|
+
* against a known protocol version). The contract is real, the
|
|
21
|
+
* recorder just cannot decode it.
|
|
22
|
+
* - opaqueScVals only -> the recorder encountered a value shape it
|
|
23
|
+
* should support but did not decode. The user cannot supply an ABI
|
|
24
|
+
* to fix a decoder bug; the guidance explicitly says so and points at
|
|
25
|
+
* re-running after a tool upgrade / reporting the path.
|
|
26
|
+
* - both -> list both diagnostics AND chain the right remediation for
|
|
27
|
+
* each (the user supplies an ABI AND reports the decoder gap).
|
|
28
|
+
* - neither (the "denom === 0" path) -> unchanged from the pre-fix
|
|
29
|
+
* text; the user did not actually hit a code-level barrier. */
|
|
17
30
|
export declare function buildLowConfidenceQuestion(c: ParseConfidence): string;
|
package/dist/record/freshness.js
CHANGED
|
@@ -36,8 +36,23 @@ export function isBelowThreshold(c) {
|
|
|
36
36
|
return c.overall < c.thresholdUsed;
|
|
37
37
|
}
|
|
38
38
|
/** Convenience: build the user-facing remediation question for an
|
|
39
|
-
* under-confidence recording.
|
|
39
|
+
* under-confidence recording.
|
|
40
|
+
*
|
|
41
|
+
* The remediation text branches on which diagnostic bucket is non-empty:
|
|
42
|
+
* - unknownContracts only -> user MUST supply an ABI (or re-capture
|
|
43
|
+
* against a known protocol version). The contract is real, the
|
|
44
|
+
* recorder just cannot decode it.
|
|
45
|
+
* - opaqueScVals only -> the recorder encountered a value shape it
|
|
46
|
+
* should support but did not decode. The user cannot supply an ABI
|
|
47
|
+
* to fix a decoder bug; the guidance explicitly says so and points at
|
|
48
|
+
* re-running after a tool upgrade / reporting the path.
|
|
49
|
+
* - both -> list both diagnostics AND chain the right remediation for
|
|
50
|
+
* each (the user supplies an ABI AND reports the decoder gap).
|
|
51
|
+
* - neither (the "denom === 0" path) -> unchanged from the pre-fix
|
|
52
|
+
* text; the user did not actually hit a code-level barrier. */
|
|
40
53
|
export function buildLowConfidenceQuestion(c) {
|
|
54
|
+
const hasUnknown = c.unknownContracts.length > 0;
|
|
55
|
+
const hasOpaque = c.opaqueScVals.length > 0;
|
|
41
56
|
const reasons = [];
|
|
42
57
|
for (const u of c.unknownContracts) {
|
|
43
58
|
reasons.push(`unknown contract ${u.contract} (${u.reason})`);
|
|
@@ -46,5 +61,20 @@ export function buildLowConfidenceQuestion(c) {
|
|
|
46
61
|
reasons.push(`opaque ScVal at ${o.path} (${o.type})`);
|
|
47
62
|
}
|
|
48
63
|
const why = reasons.length === 0 ? 'no diagnostic reason available' : reasons.join('; ');
|
|
49
|
-
|
|
64
|
+
const header = `Recording refused: parseConfidence ${c.overall.toFixed(3)} is below the threshold ${c.thresholdUsed.toFixed(3)}. Diagnostic: ${why}.`;
|
|
65
|
+
// The two remediation branches are explicit so the user (or the agent)
|
|
66
|
+
// can dispatch on the verb. The "unknown contract" branch is the only
|
|
67
|
+
// one that asks for an ABI; the "opaque ScVal" branch asks the user to
|
|
68
|
+
// report the path and re-run after a tool upgrade.
|
|
69
|
+
if (hasUnknown && hasOpaque) {
|
|
70
|
+
return (`${header} Supply an ABI for the unknown contract(s) or re-capture the transaction against a known protocol version; ` +
|
|
71
|
+
`also file the opaque ScVal path against the recorder (decoder limitation) and re-run record_transaction after a tool upgrade.`);
|
|
72
|
+
}
|
|
73
|
+
if (hasUnknown) {
|
|
74
|
+
return `${header} Supply an ABI for the unknown contract(s) or re-capture the transaction against a known protocol version, then re-run record_transaction.`;
|
|
75
|
+
}
|
|
76
|
+
if (hasOpaque) {
|
|
77
|
+
return `${header} This is a recorder decoder limitation (the value shape is supported in principle but not yet decoded) - the agent cannot fix it by supplying an ABI. Report the opaque ScVal path above against the recorder and re-run record_transaction after a tool upgrade.`;
|
|
78
|
+
}
|
|
79
|
+
return `${header} No actionable diagnostic was recorded; re-run record_transaction against a fresh fetch and inspect the events.`;
|
|
50
80
|
}
|
package/dist/record/index.d.ts
CHANGED
|
@@ -5,6 +5,9 @@ import { type RpcFetcher } from './rpc.ts';
|
|
|
5
5
|
* - exactly one of `hash` / `xdr`
|
|
6
6
|
* - `network` for hash mode (selects the public RPC)
|
|
7
7
|
* - optional injected `fetcher` for tests + custom RPC endpoints
|
|
8
|
+
* - optional `crossNetworkFetcher` (item 2) for tests + offline use;
|
|
9
|
+
* production builds the cross-network probe from `createRpcServer` when
|
|
10
|
+
* this field is absent
|
|
8
11
|
* - optional `confidenceOverride` to relax the gate (default 1.0)
|
|
9
12
|
*
|
|
10
13
|
* In xdr mode `network` is still required so the caller documents intent
|
|
@@ -15,6 +18,14 @@ export interface RecordInput {
|
|
|
15
18
|
xdr?: string;
|
|
16
19
|
network: Network;
|
|
17
20
|
fetcher?: RpcFetcher;
|
|
21
|
+
/** Test-only seam + explicit production override for the cross-network
|
|
22
|
+
* probe (item 2). When the primary fetcher returns null on hash mode,
|
|
23
|
+
* the recorder also tries this fetcher against the OTHER network. If it
|
|
24
|
+
* finds the hash there, the recorder returns a specific actionable
|
|
25
|
+
* error mentioning the actual network. When unset, the recorder builds
|
|
26
|
+
* this fetcher from `createRpcServer(otherNetwork)` so the help fires
|
|
27
|
+
* automatically; tests can pass a deterministic stub. */
|
|
28
|
+
crossNetworkFetcher?: RpcFetcher;
|
|
18
29
|
confidenceOverride?: number;
|
|
19
30
|
}
|
|
20
31
|
export type RecordResult = ToolResponse<RecordedTransaction>;
|
package/dist/record/index.js
CHANGED
|
@@ -50,6 +50,21 @@ export async function recordTransaction(input) {
|
|
|
50
50
|
return err('RECORDING_FAILED', 'hash required for on-chain mode', false);
|
|
51
51
|
const fetched = await fetcher(hash);
|
|
52
52
|
if (!fetched) {
|
|
53
|
+
// Item 2: cross-network sanity check. The default NOT_FOUND message
|
|
54
|
+
// ("transaction X not found on <network>") is not actionable when the
|
|
55
|
+
// user just used the wrong --network. Probe the OTHER network's
|
|
56
|
+
// fetcher before giving up: if the hash actually exists there, the
|
|
57
|
+
// user almost certainly passed the wrong network flag. Trade-off:
|
|
58
|
+
// one extra RPC round-trip ONLY on the NOT_FOUND path (the happy
|
|
59
|
+
// path is unchanged; NOT_FOUND is rare). The probe is auto-built
|
|
60
|
+
// from createRpcServer(otherNetwork) when the caller did not inject
|
|
61
|
+
// one; tests pass an explicit stub for offline determinism.
|
|
62
|
+
const otherNetwork = input.network === 'mainnet' ? 'testnet' : 'mainnet';
|
|
63
|
+
const crossFetcher = input.crossNetworkFetcher ?? createRpcServer(otherNetwork);
|
|
64
|
+
const crossFetched = await crossFetcher(hash);
|
|
65
|
+
if (crossFetched) {
|
|
66
|
+
return err('RECORDING_FAILED', `transaction ${hash} not found on ${input.network}; it exists on ${otherNetwork}. Re-run with --network ${otherNetwork} (or the corresponding MCP / API field).`, true);
|
|
67
|
+
}
|
|
53
68
|
return err('RECORDING_FAILED', `transaction ${hash} not found on ${input.network}`, true);
|
|
54
69
|
}
|
|
55
70
|
const events = combineEvents(fetched.events);
|
|
@@ -125,6 +140,16 @@ function finish(network, decoded, confidenceOverride) {
|
|
|
125
140
|
unknownContracts: decoded.unknownContracts,
|
|
126
141
|
opaqueScVals: decoded.opaqueScVals,
|
|
127
142
|
});
|
|
143
|
+
// Item 1: surface the "zero contract invocations decoded" case explicitly.
|
|
144
|
+
// The math already pins overall = 1.0 via the `denom === 0` guard; the
|
|
145
|
+
// marker just makes the silence visible so a downstream synth can refuse
|
|
146
|
+
// the recording (a policy must scope to an authorized contract call)
|
|
147
|
+
// without inferring the situation from `invocations: []` next to a
|
|
148
|
+
// confidence of 1.0. Only set when zero invocations were actually decoded
|
|
149
|
+
// - the marker is a positive signal, not a default.
|
|
150
|
+
if (decoded.invocations.length === 0) {
|
|
151
|
+
confidence = { ...confidence, noInvocations: true };
|
|
152
|
+
}
|
|
128
153
|
if (typeof confidenceOverride === 'number') {
|
|
129
154
|
confidence = { ...confidence, thresholdUsed: confidenceOverride };
|
|
130
155
|
}
|
|
@@ -9,9 +9,21 @@ export declare function extractTokenMovements(events: OnChainEvent[], opaqueScVa
|
|
|
9
9
|
}>): TokenMovement[];
|
|
10
10
|
/** Read an I128 amount from an event data ScVal. Supports:
|
|
11
11
|
* - data is I128 directly
|
|
12
|
-
* - data is
|
|
13
|
-
* - data is a Vec whose [0]
|
|
14
|
-
* - data is
|
|
12
|
+
* - data is U64 (fallback for amount fields encoded as u64)
|
|
13
|
+
* - data is a Vec whose [0] (or [1] for SAC transfer) is I128 / U64
|
|
14
|
+
* - data is a Vec whose [0] is a Map { 'amount': I128 | U64 }
|
|
15
|
+
* - data is a Map whose entry with key "amount" is I128 / U64
|
|
16
|
+
*
|
|
17
|
+
* The Map shapes are the canonical data layout for SAC/SEP-41 `transfer`,
|
|
18
|
+
* `mint`, and `burn` events on Stellar mainnet, e.g. the USDC SAC and every
|
|
19
|
+
* Stellar Asset Contract. The key `amount` is the SEP-41-defined field name
|
|
20
|
+
* (verified empirically against txs 112d2392..., eb39f493..., 50d36f5c...
|
|
21
|
+
* where the data is `Map { amount: I128, to_muxed_id: ... }`). The other
|
|
22
|
+
* Map entries we observe in the wild (to_muxed_id, from_muxed_id) are
|
|
23
|
+
* muxed-address adjuncts that the recorder does not support as topic
|
|
24
|
+
* addresses; they are ignored here on purpose. A Map without an `amount`
|
|
25
|
+
* entry, or with an `amount` entry that is not I128/U64, returns null
|
|
26
|
+
* (fail-closed). */
|
|
15
27
|
export declare function readAmount(data: OnChainEvent['data']): string | null;
|
|
16
28
|
/** Re-decode a single ContractEvent body into a TokenMovement-shaped tuple,
|
|
17
29
|
* given the raw xdr.ScVal form (NOT the OnChainEvent subset). Used by the
|
package/dist/record/movements.js
CHANGED
|
@@ -81,25 +81,60 @@ function parseSingleEvent(evt) {
|
|
|
81
81
|
}
|
|
82
82
|
/** Read an I128 amount from an event data ScVal. Supports:
|
|
83
83
|
* - data is I128 directly
|
|
84
|
-
* - data is
|
|
85
|
-
* - data is a Vec whose [0]
|
|
86
|
-
* - data is
|
|
84
|
+
* - data is U64 (fallback for amount fields encoded as u64)
|
|
85
|
+
* - data is a Vec whose [0] (or [1] for SAC transfer) is I128 / U64
|
|
86
|
+
* - data is a Vec whose [0] is a Map { 'amount': I128 | U64 }
|
|
87
|
+
* - data is a Map whose entry with key "amount" is I128 / U64
|
|
88
|
+
*
|
|
89
|
+
* The Map shapes are the canonical data layout for SAC/SEP-41 `transfer`,
|
|
90
|
+
* `mint`, and `burn` events on Stellar mainnet, e.g. the USDC SAC and every
|
|
91
|
+
* Stellar Asset Contract. The key `amount` is the SEP-41-defined field name
|
|
92
|
+
* (verified empirically against txs 112d2392..., eb39f493..., 50d36f5c...
|
|
93
|
+
* where the data is `Map { amount: I128, to_muxed_id: ... }`). The other
|
|
94
|
+
* Map entries we observe in the wild (to_muxed_id, from_muxed_id) are
|
|
95
|
+
* muxed-address adjuncts that the recorder does not support as topic
|
|
96
|
+
* addresses; they are ignored here on purpose. A Map without an `amount`
|
|
97
|
+
* entry, or with an `amount` entry that is not I128/U64, returns null
|
|
98
|
+
* (fail-closed). */
|
|
87
99
|
export function readAmount(data) {
|
|
88
100
|
if (data.type === 'i128')
|
|
89
101
|
return data.value;
|
|
90
102
|
if (data.type === 'u64')
|
|
91
103
|
return data.value;
|
|
104
|
+
if (data.type === 'map')
|
|
105
|
+
return readAmountFromMap(data.value);
|
|
92
106
|
if (data.type !== 'vec')
|
|
93
107
|
return null;
|
|
94
|
-
|
|
95
|
-
// SAC transfer event data
|
|
96
|
-
|
|
108
|
+
// Vec shape: walk the elements and accept the first I128/U64 OR a Map whose
|
|
109
|
+
// 'amount' entry is I128/U64. SAC transfer event data is sometimes
|
|
110
|
+
// Vec<Address, I128> and sometimes Vec<Map{amount: I128}, ...>; both reach
|
|
111
|
+
// the same code path below.
|
|
112
|
+
for (const item of data.value) {
|
|
97
113
|
if (item.type === 'i128')
|
|
98
114
|
return item.value;
|
|
99
115
|
if (item.type === 'u64')
|
|
100
116
|
return item.value;
|
|
117
|
+
if (item.type === 'map') {
|
|
118
|
+
const fromMap = readAmountFromMap(item.value);
|
|
119
|
+
if (fromMap !== null)
|
|
120
|
+
return fromMap;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
/** Try to extract an I128/U64 amount from a Map's `amount` entry. Returns
|
|
126
|
+
* null when the Map lacks an `amount` key or the entry is not a numeric
|
|
127
|
+
* ScVal kind this recorder understands. */
|
|
128
|
+
function readAmountFromMap(entries) {
|
|
129
|
+
for (const entry of entries) {
|
|
130
|
+
if (entry.key !== 'amount')
|
|
131
|
+
continue;
|
|
132
|
+
if (entry.val.type === 'i128')
|
|
133
|
+
return entry.val.value;
|
|
134
|
+
if (entry.val.type === 'u64')
|
|
135
|
+
return entry.val.value;
|
|
136
|
+
return null;
|
|
101
137
|
}
|
|
102
|
-
// Some flavours wrap in a Map. We expose Map entries as other; bail.
|
|
103
138
|
return null;
|
|
104
139
|
}
|
|
105
140
|
/** Extract a single address from an event data ScVal at the given vec index. */
|
package/dist/run/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ProposedPolicy, type RecordedTransaction, type ToolResponse } from '../index.ts';
|
|
1
|
+
import { type ErrorCode, type ProposedPolicy, type RecordedTransaction, type ToolError, type ToolResponse } from '../index.ts';
|
|
2
2
|
import { type RecordTransactionInput, type SynthesizePolicyInput } from './schemas.ts';
|
|
3
3
|
export type { RecordTransactionInput, SynthesizePolicyInput } from './schemas.ts';
|
|
4
4
|
export { ComposeUserResponsesSchema, InterpreterOptionsSchema, MandateSpecSchema, NetworkSchema, OzAdapterConfigSchema, RecordedTransactionSchema, RecordTransactionInputSchema, SynthesizePolicyInputSchema, ToolErrorSchema, } from './schemas.ts';
|
|
@@ -29,3 +29,15 @@ export declare function runRecordTransaction(raw: unknown): Promise<ToolResponse
|
|
|
29
29
|
* XDR decode error in the adapter) would otherwise surface as
|
|
30
30
|
* "[object Object]" in the MCP transport. */
|
|
31
31
|
export declare function runSynthesizePolicy(raw: unknown): Promise<ToolResponse<ProposedPolicy>>;
|
|
32
|
+
/** Build a canonical ToolError for a thrown exception caught by the tool
|
|
33
|
+
* envelope. The MCP SDK stringifies thrown objects as "[object Object]" by
|
|
34
|
+
* default, so we extract a string-friendly message and tag the original
|
|
35
|
+
* error in `details` for the agent to inspect. The `code` is the tool's
|
|
36
|
+
* domain code (RECORDING_FAILED for `record_transaction`, SYNTHESIS_ERROR
|
|
37
|
+
* for `synthesize_policy`) so the agent dispatches on the same code the
|
|
38
|
+
* structured ToolError would carry.
|
|
39
|
+
*
|
|
40
|
+
* Exported as `_caughtError` (the leading underscore signals the test-only
|
|
41
|
+
* seam) so the suite in run/index.test.ts can drive the envelope path
|
|
42
|
+
* without standing up a full recordTransaction pipeline. */
|
|
43
|
+
export declare function caughtError(toolName: 'record_transaction' | 'synthesize_policy', code: ErrorCode, e: unknown): ToolError;
|
package/dist/run/index.js
CHANGED
|
@@ -134,23 +134,19 @@ function validationError(toolName, issues) {
|
|
|
134
134
|
};
|
|
135
135
|
}
|
|
136
136
|
/** Build a canonical ToolError for a thrown exception caught by the tool
|
|
137
|
-
* envelope.
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* for `
|
|
142
|
-
*
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
message = `${toolName}: caught non-Error throw of type ${typeof e}`;
|
|
153
|
-
}
|
|
137
|
+
* envelope. The MCP SDK stringifies thrown objects as "[object Object]" by
|
|
138
|
+
* default, so we extract a string-friendly message and tag the original
|
|
139
|
+
* error in `details` for the agent to inspect. The `code` is the tool's
|
|
140
|
+
* domain code (RECORDING_FAILED for `record_transaction`, SYNTHESIS_ERROR
|
|
141
|
+
* for `synthesize_policy`) so the agent dispatches on the same code the
|
|
142
|
+
* structured ToolError would carry.
|
|
143
|
+
*
|
|
144
|
+
* Exported as `_caughtError` (the leading underscore signals the test-only
|
|
145
|
+
* seam) so the suite in run/index.test.ts can drive the envelope path
|
|
146
|
+
* without standing up a full recordTransaction pipeline. */
|
|
147
|
+
export function caughtError(toolName, code, e) {
|
|
148
|
+
const message = describeThrown(e, toolName);
|
|
149
|
+
const details = { thrown: safeStringify(e) };
|
|
154
150
|
return {
|
|
155
151
|
code,
|
|
156
152
|
message: `${toolName}: unhandled throw escaped core envelope: ${message}`,
|
|
@@ -159,6 +155,68 @@ function caughtError(toolName, code, e) {
|
|
|
159
155
|
remediation: {
|
|
160
156
|
toolCall: { name: toolName, args: {} },
|
|
161
157
|
},
|
|
162
|
-
details
|
|
158
|
+
details,
|
|
163
159
|
};
|
|
164
160
|
}
|
|
161
|
+
/** Build a human-readable message for an unknown caught value. Order matters:
|
|
162
|
+
* 1. `Error` instances use `.message` (or fallback to the class name).
|
|
163
|
+
* 2. Native strings pass through verbatim.
|
|
164
|
+
* 3. Objects with a string `message` field use that field (mirrors the
|
|
165
|
+
* shape thrown by some SDKs that package errors as plain objects).
|
|
166
|
+
* 4. Anything else falls back to a JSON-shaped summary, never to
|
|
167
|
+
* "[object Object]".
|
|
168
|
+
* The full payload is also captured in `details.thrown` via
|
|
169
|
+
* `safeStringify` so the agent can inspect the original value without
|
|
170
|
+
* risking an infinite loop on circular refs. */
|
|
171
|
+
function describeThrown(e, toolName) {
|
|
172
|
+
if (e instanceof Error) {
|
|
173
|
+
return e.message || `${toolName}: ${e.name || 'Error'}`;
|
|
174
|
+
}
|
|
175
|
+
if (typeof e === 'string') {
|
|
176
|
+
return e;
|
|
177
|
+
}
|
|
178
|
+
if (e !== null && typeof e === 'object') {
|
|
179
|
+
const obj = e;
|
|
180
|
+
const m = obj.message;
|
|
181
|
+
if (typeof m === 'string' && m.length > 0) {
|
|
182
|
+
return truncate(m);
|
|
183
|
+
}
|
|
184
|
+
return truncate(safeStringify(e));
|
|
185
|
+
}
|
|
186
|
+
return `${toolName}: caught non-Error throw of type ${typeof e}`;
|
|
187
|
+
}
|
|
188
|
+
/** Hard cap on the human-readable message embedded in the ToolError. The full
|
|
189
|
+
* payload is still preserved in `details.thrown` so the agent can inspect
|
|
190
|
+
* it - the truncation is only to keep the top-level message small enough to
|
|
191
|
+
* fit comfortably in transport logs. */
|
|
192
|
+
const MAX_MESSAGE_LEN = 512;
|
|
193
|
+
/** JSON.stringify that survives circular refs and very large payloads. The
|
|
194
|
+
* output is itself bounded by the same `MAX_DETAILS_LEN` so a thrown object
|
|
195
|
+
* with megabytes of buffer data cannot bloat the WHOLE envelope. */
|
|
196
|
+
const MAX_DETAILS_LEN = 4096;
|
|
197
|
+
function safeStringify(v) {
|
|
198
|
+
const seen = new WeakSet();
|
|
199
|
+
const json = JSON.stringify(v, (_k, value) => {
|
|
200
|
+
if (typeof value === 'bigint')
|
|
201
|
+
return value.toString();
|
|
202
|
+
if (typeof value === 'function')
|
|
203
|
+
return `[function ${value.name || 'anonymous'}]`;
|
|
204
|
+
if (value instanceof Error) {
|
|
205
|
+
return { name: value.name, message: value.message, stack: value.stack };
|
|
206
|
+
}
|
|
207
|
+
if (value !== null && typeof value === 'object') {
|
|
208
|
+
if (seen.has(value))
|
|
209
|
+
return '[Circular]';
|
|
210
|
+
seen.add(value);
|
|
211
|
+
}
|
|
212
|
+
return value;
|
|
213
|
+
}, 2);
|
|
214
|
+
if (json === undefined)
|
|
215
|
+
return '<unserializable>';
|
|
216
|
+
return truncate(json, MAX_DETAILS_LEN);
|
|
217
|
+
}
|
|
218
|
+
function truncate(s, cap = MAX_MESSAGE_LEN) {
|
|
219
|
+
if (s.length <= cap)
|
|
220
|
+
return s;
|
|
221
|
+
return `${s.slice(0, cap)}\n…[truncated ${s.length - cap} chars]`;
|
|
222
|
+
}
|
package/dist/run/schemas.d.ts
CHANGED
|
@@ -446,16 +446,19 @@ export declare const ComposeUserResponsesSchema: z.ZodObject<{
|
|
|
446
446
|
validUntilLedger: z.ZodOptional<z.ZodNumber>;
|
|
447
447
|
limitAmount: z.ZodOptional<z.ZodString>;
|
|
448
448
|
invocationLimit: z.ZodOptional<z.ZodNumber>;
|
|
449
|
+
swapRecipientAllowlist: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
|
|
449
450
|
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
|
|
450
451
|
windowSeconds: z.ZodOptional<z.ZodNumber>;
|
|
451
452
|
validUntilLedger: z.ZodOptional<z.ZodNumber>;
|
|
452
453
|
limitAmount: z.ZodOptional<z.ZodString>;
|
|
453
454
|
invocationLimit: z.ZodOptional<z.ZodNumber>;
|
|
455
|
+
swapRecipientAllowlist: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
|
|
454
456
|
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
455
457
|
windowSeconds: z.ZodOptional<z.ZodNumber>;
|
|
456
458
|
validUntilLedger: z.ZodOptional<z.ZodNumber>;
|
|
457
459
|
limitAmount: z.ZodOptional<z.ZodString>;
|
|
458
460
|
invocationLimit: z.ZodOptional<z.ZodNumber>;
|
|
461
|
+
swapRecipientAllowlist: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
|
|
459
462
|
}, z.ZodTypeAny, "passthrough">>;
|
|
460
463
|
/** OzAdapterConfig - the per-network OZ built-in instance addresses. */
|
|
461
464
|
export declare const OzAdapterConfigSchema: z.ZodObject<{
|
|
@@ -1003,16 +1006,19 @@ export declare const SynthesizePolicyRecordingInputSchema: z.ZodObject<{
|
|
|
1003
1006
|
validUntilLedger: z.ZodOptional<z.ZodNumber>;
|
|
1004
1007
|
limitAmount: z.ZodOptional<z.ZodString>;
|
|
1005
1008
|
invocationLimit: z.ZodOptional<z.ZodNumber>;
|
|
1009
|
+
swapRecipientAllowlist: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
|
|
1006
1010
|
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
|
|
1007
1011
|
windowSeconds: z.ZodOptional<z.ZodNumber>;
|
|
1008
1012
|
validUntilLedger: z.ZodOptional<z.ZodNumber>;
|
|
1009
1013
|
limitAmount: z.ZodOptional<z.ZodString>;
|
|
1010
1014
|
invocationLimit: z.ZodOptional<z.ZodNumber>;
|
|
1015
|
+
swapRecipientAllowlist: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
|
|
1011
1016
|
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
1012
1017
|
windowSeconds: z.ZodOptional<z.ZodNumber>;
|
|
1013
1018
|
validUntilLedger: z.ZodOptional<z.ZodNumber>;
|
|
1014
1019
|
limitAmount: z.ZodOptional<z.ZodString>;
|
|
1015
1020
|
invocationLimit: z.ZodOptional<z.ZodNumber>;
|
|
1021
|
+
swapRecipientAllowlist: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
|
|
1016
1022
|
}, z.ZodTypeAny, "passthrough">>>;
|
|
1017
1023
|
confidenceOverride: z.ZodOptional<z.ZodObject<{
|
|
1018
1024
|
threshold: z.ZodNumber;
|
|
@@ -1130,6 +1136,7 @@ export declare const SynthesizePolicyRecordingInputSchema: z.ZodObject<{
|
|
|
1130
1136
|
validUntilLedger: z.ZodOptional<z.ZodNumber>;
|
|
1131
1137
|
limitAmount: z.ZodOptional<z.ZodString>;
|
|
1132
1138
|
invocationLimit: z.ZodOptional<z.ZodNumber>;
|
|
1139
|
+
swapRecipientAllowlist: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
|
|
1133
1140
|
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
1134
1141
|
confidenceOverride?: {
|
|
1135
1142
|
threshold: number;
|
|
@@ -1193,6 +1200,7 @@ export declare const SynthesizePolicyRecordingInputSchema: z.ZodObject<{
|
|
|
1193
1200
|
validUntilLedger: z.ZodOptional<z.ZodNumber>;
|
|
1194
1201
|
limitAmount: z.ZodOptional<z.ZodString>;
|
|
1195
1202
|
invocationLimit: z.ZodOptional<z.ZodNumber>;
|
|
1203
|
+
swapRecipientAllowlist: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
|
|
1196
1204
|
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
1197
1205
|
confidenceOverride?: {
|
|
1198
1206
|
threshold: number;
|
|
@@ -1649,16 +1657,19 @@ export declare const SynthesizePolicyInputSchema: z.ZodDiscriminatedUnion<"sourc
|
|
|
1649
1657
|
validUntilLedger: z.ZodOptional<z.ZodNumber>;
|
|
1650
1658
|
limitAmount: z.ZodOptional<z.ZodString>;
|
|
1651
1659
|
invocationLimit: z.ZodOptional<z.ZodNumber>;
|
|
1660
|
+
swapRecipientAllowlist: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
|
|
1652
1661
|
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
|
|
1653
1662
|
windowSeconds: z.ZodOptional<z.ZodNumber>;
|
|
1654
1663
|
validUntilLedger: z.ZodOptional<z.ZodNumber>;
|
|
1655
1664
|
limitAmount: z.ZodOptional<z.ZodString>;
|
|
1656
1665
|
invocationLimit: z.ZodOptional<z.ZodNumber>;
|
|
1666
|
+
swapRecipientAllowlist: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
|
|
1657
1667
|
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
1658
1668
|
windowSeconds: z.ZodOptional<z.ZodNumber>;
|
|
1659
1669
|
validUntilLedger: z.ZodOptional<z.ZodNumber>;
|
|
1660
1670
|
limitAmount: z.ZodOptional<z.ZodString>;
|
|
1661
1671
|
invocationLimit: z.ZodOptional<z.ZodNumber>;
|
|
1672
|
+
swapRecipientAllowlist: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
|
|
1662
1673
|
}, z.ZodTypeAny, "passthrough">>>;
|
|
1663
1674
|
confidenceOverride: z.ZodOptional<z.ZodObject<{
|
|
1664
1675
|
threshold: z.ZodNumber;
|
|
@@ -1776,6 +1787,7 @@ export declare const SynthesizePolicyInputSchema: z.ZodDiscriminatedUnion<"sourc
|
|
|
1776
1787
|
validUntilLedger: z.ZodOptional<z.ZodNumber>;
|
|
1777
1788
|
limitAmount: z.ZodOptional<z.ZodString>;
|
|
1778
1789
|
invocationLimit: z.ZodOptional<z.ZodNumber>;
|
|
1790
|
+
swapRecipientAllowlist: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
|
|
1779
1791
|
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
1780
1792
|
confidenceOverride?: {
|
|
1781
1793
|
threshold: number;
|
|
@@ -1839,6 +1851,7 @@ export declare const SynthesizePolicyInputSchema: z.ZodDiscriminatedUnion<"sourc
|
|
|
1839
1851
|
validUntilLedger: z.ZodOptional<z.ZodNumber>;
|
|
1840
1852
|
limitAmount: z.ZodOptional<z.ZodString>;
|
|
1841
1853
|
invocationLimit: z.ZodOptional<z.ZodNumber>;
|
|
1854
|
+
swapRecipientAllowlist: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
|
|
1842
1855
|
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
1843
1856
|
confidenceOverride?: {
|
|
1844
1857
|
threshold: number;
|
package/dist/run/schemas.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// imports them here so its tool-shape bindings stay in step; the CLI imports
|
|
16
16
|
// them here so it can build the same args envelope the MCP transport builds.
|
|
17
17
|
import { z } from 'zod';
|
|
18
|
+
import { isStellarAddress } from "../synth/address.js";
|
|
18
19
|
/** Soroban `valid_until` is a u32 ledger sequence; a value above this cannot be
|
|
19
20
|
* installed on-chain, so reject it at the boundary (fail-closed). */
|
|
20
21
|
const U32_MAX = 4294967295;
|
|
@@ -126,6 +127,13 @@ export const ComposeUserResponsesSchema = z
|
|
|
126
127
|
.regex(/^[0-9]+$/)
|
|
127
128
|
.optional(),
|
|
128
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(),
|
|
129
137
|
})
|
|
130
138
|
.passthrough();
|
|
131
139
|
/** OzAdapterConfig - the per-network OZ built-in instance addresses. */
|
|
@@ -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;
|