@crediolabs/policy-synth 0.2.0 → 0.3.0
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/README.md +4 -4
- package/dist/adapters/interpreter/adapter.d.ts +19 -25
- package/dist/adapters/interpreter/adapter.js +54 -487
- package/dist/adapters/interpreter/index.d.ts +1 -1
- package/dist/adapters/interpreter/index.js +1 -1
- package/dist/adapters/oz/adapter.js +1 -15
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +1 -4
- package/dist/index.js +1 -4
- package/dist/install/build-add-context-rule.d.ts +3 -19
- package/dist/install/build-add-context-rule.js +3 -98
- package/dist/install/build-install-policy.d.ts +4 -53
- package/dist/install/build-install-policy.js +44 -123
- package/dist/install/index.d.ts +0 -2
- package/dist/install/index.js +0 -7
- package/dist/ir/index.d.ts +1 -1
- package/dist/ir/types.d.ts +7 -76
- package/dist/ir/types.js +0 -2
- package/dist/predicate/decode.d.ts +1 -1
- package/dist/predicate/decode.js +2 -70
- package/dist/predicate/encode.js +45 -229
- package/dist/predicate/from-json.js +1 -42
- package/dist/record/decode.js +3 -8
- package/dist/review-card/builder.d.ts +4 -3
- package/dist/review-card/builder.js +18 -162
- package/dist/review-card/cross-check.js +2 -105
- package/dist/review-card/render-leaf.d.ts +4 -0
- package/dist/review-card/render-leaf.js +47 -0
- package/dist/run/index.d.ts +36 -139
- package/dist/run/index.js +125 -437
- package/dist/run/schemas.d.ts +168 -1929
- package/dist/run/schemas.js +40 -269
- package/dist/seams/types.d.ts +0 -2
- package/dist/simulate/deny-cases.d.ts +26 -0
- package/dist/simulate/deny-cases.js +282 -0
- package/dist/simulate/evaluate.d.ts +18 -0
- package/dist/simulate/evaluate.js +271 -0
- package/dist/simulate/index.d.ts +4 -0
- package/dist/simulate/index.js +8 -0
- package/dist/synth/compose-from-recording.d.ts +24 -51
- package/dist/synth/compose-from-recording.js +120 -282
- package/dist/synth/deny-cases.d.ts +5 -7
- package/dist/synth/deny-cases.js +23 -373
- package/dist/synth/evaluate.d.ts +0 -21
- package/dist/synth/evaluate.js +56 -336
- package/dist/synth/index.d.ts +0 -5
- package/dist/synth/index.js +0 -5
- package/dist/synth/permit-context.d.ts +2 -9
- package/dist/synth/permit-context.js +1 -82
- package/dist/synth/synthesize-from-recording.d.ts +1 -29
- package/dist/synth/synthesize-from-recording.js +41 -356
- package/dist/types.d.ts +8 -60
- package/dist/types.js +5 -2
- package/dist/verify/simulate.d.ts +1 -14
- package/dist/verify/simulate.js +2 -88
- package/dist/verify/verify.d.ts +1 -7
- package/dist/verify/verify.js +0 -8
- package/dist-cjs/adapters/interpreter/adapter.d.ts +19 -25
- package/dist-cjs/adapters/interpreter/adapter.js +56 -489
- package/dist-cjs/adapters/interpreter/index.d.ts +1 -1
- package/dist-cjs/adapters/interpreter/index.js +1 -2
- package/dist-cjs/adapters/oz/adapter.js +1 -15
- package/dist-cjs/errors.d.ts +1 -1
- package/dist-cjs/index.d.ts +1 -4
- package/dist-cjs/index.js +1 -4
- package/dist-cjs/install/build-add-context-rule.d.ts +3 -19
- package/dist-cjs/install/build-add-context-rule.js +1 -97
- package/dist-cjs/install/build-install-policy.d.ts +4 -53
- package/dist-cjs/install/build-install-policy.js +44 -122
- package/dist-cjs/install/index.d.ts +0 -2
- package/dist-cjs/install/index.js +2 -23
- package/dist-cjs/ir/index.d.ts +1 -1
- package/dist-cjs/ir/types.d.ts +7 -76
- package/dist-cjs/ir/types.js +0 -2
- package/dist-cjs/predicate/decode.d.ts +1 -1
- package/dist-cjs/predicate/decode.js +2 -70
- package/dist-cjs/predicate/encode.js +45 -229
- package/dist-cjs/predicate/from-json.js +1 -42
- package/dist-cjs/record/decode.js +3 -8
- package/dist-cjs/review-card/builder.d.ts +4 -3
- package/dist-cjs/review-card/builder.js +23 -167
- package/dist-cjs/review-card/cross-check.js +7 -110
- package/dist-cjs/review-card/render-leaf.d.ts +4 -0
- package/dist-cjs/review-card/render-leaf.js +52 -0
- package/dist-cjs/run/index.d.ts +36 -139
- package/dist-cjs/run/index.js +125 -444
- package/dist-cjs/run/schemas.d.ts +168 -1929
- package/dist-cjs/run/schemas.js +41 -270
- package/dist-cjs/seams/types.d.ts +0 -2
- package/dist-cjs/simulate/deny-cases.d.ts +26 -0
- package/dist-cjs/simulate/deny-cases.js +286 -0
- package/dist-cjs/simulate/evaluate.d.ts +18 -0
- package/dist-cjs/simulate/evaluate.js +274 -0
- package/dist-cjs/simulate/index.d.ts +4 -0
- package/dist-cjs/simulate/index.js +13 -0
- package/dist-cjs/synth/compose-from-recording.d.ts +24 -51
- package/dist-cjs/synth/compose-from-recording.js +120 -282
- package/dist-cjs/synth/deny-cases.d.ts +5 -7
- package/dist-cjs/synth/deny-cases.js +23 -374
- package/dist-cjs/synth/evaluate.d.ts +0 -21
- package/dist-cjs/synth/evaluate.js +57 -337
- package/dist-cjs/synth/index.d.ts +0 -5
- package/dist-cjs/synth/index.js +1 -11
- package/dist-cjs/synth/permit-context.d.ts +2 -9
- package/dist-cjs/synth/permit-context.js +1 -82
- package/dist-cjs/synth/synthesize-from-recording.d.ts +1 -29
- package/dist-cjs/synth/synthesize-from-recording.js +39 -354
- package/dist-cjs/types.d.ts +8 -60
- package/dist-cjs/types.js +6 -3
- package/dist-cjs/verify/simulate.d.ts +1 -14
- package/dist-cjs/verify/simulate.js +2 -88
- package/dist-cjs/verify/verify.d.ts +1 -7
- package/dist-cjs/verify/verify.js +0 -8
- package/package.json +4 -4
- package/src/adapters/interpreter/adapter.ts +76 -572
- package/src/errors.ts +0 -19
- package/src/index.ts +1 -4
- package/src/install/build-add-context-rule.ts +5 -139
- package/src/install/build-install-policy.ts +93 -214
- package/src/install/index.ts +0 -34
- package/src/predicate/decode.ts +2 -70
- package/src/predicate/encode.ts +49 -261
- package/src/predicate/from-json.ts +1 -43
- package/src/record/decode.ts +3 -8
- package/src/review-card/builder.ts +19 -168
- package/src/review-card/cross-check.ts +3 -105
- package/src/review-card/render-leaf.ts +48 -0
- package/src/run/index.ts +144 -572
- package/src/run/schemas.ts +42 -291
- package/src/simulate/deny-cases.ts +334 -0
- package/src/simulate/evaluate.ts +284 -0
- package/src/simulate/index.ts +11 -0
- package/src/synth/compose-from-recording.ts +148 -347
- package/src/synth/index.ts +0 -13
- package/src/synth/synthesize-from-recording.ts +43 -456
- package/src/types.ts +13 -49
- package/dist/install/authority-overlap.d.ts +0 -134
- package/dist/install/authority-overlap.js +0 -0
- package/dist/install/build-install-predicate.d.ts +0 -96
- package/dist/install/build-install-predicate.js +0 -444
- package/dist/install/build-merge-policy.d.ts +0 -70
- package/dist/install/build-merge-policy.js +0 -130
- package/dist/install/plan-merge-policy.d.ts +0 -49
- package/dist/install/plan-merge-policy.js +0 -86
- package/dist/install/read-account-rules.d.ts +0 -100
- package/dist/install/read-account-rules.js +0 -283
- package/dist-cjs/install/authority-overlap.d.ts +0 -134
- package/dist-cjs/install/authority-overlap.js +0 -0
- package/dist-cjs/install/build-install-predicate.d.ts +0 -96
- package/dist-cjs/install/build-install-predicate.js +0 -479
- package/dist-cjs/install/build-merge-policy.d.ts +0 -70
- package/dist-cjs/install/build-merge-policy.js +0 -134
- package/dist-cjs/install/plan-merge-policy.d.ts +0 -49
- package/dist-cjs/install/plan-merge-policy.js +0 -90
- package/dist-cjs/install/read-account-rules.d.ts +0 -100
- package/dist-cjs/install/read-account-rules.js +0 -296
- package/src/adapters/interpreter/index.ts +0 -8
- package/src/adapters/oz/adapter.ts +0 -376
- package/src/adapters/oz/index.ts +0 -9
- package/src/codegen/compile-gate.ts +0 -167
- package/src/codegen/index.ts +0 -17
- package/src/codegen/template.ts +0 -165
- package/src/install/authority-overlap.ts +0 -0
- package/src/install/build-merge-policy.ts +0 -219
- package/src/install/plan-merge-policy.ts +0 -133
- package/src/install/read-account-rules.ts +0 -376
- package/src/ir/index.ts +0 -13
- package/src/ir/types.ts +0 -132
- package/src/mandate/index.ts +0 -4
- package/src/mandate/to-ir.ts +0 -71
- package/src/mandate/types.ts +0 -21
- package/src/seams/index.ts +0 -11
- package/src/seams/types.ts +0 -81
- package/src/synth/deny-cases.ts +0 -663
- package/src/synth/evaluate.ts +0 -613
- package/src/synth/harness.ts +0 -68
- package/src/synth/minimize.ts +0 -48
- package/src/synth/permit-context.ts +0 -136
- package/src/synth/predicate-literals.ts +0 -27
- package/src/synth/synthesize-from-mandate.ts +0 -82
- package/src/verify/envelope.ts +0 -28
- package/src/verify/index.ts +0 -5
- package/src/verify/simulate.ts +0 -311
- package/src/verify/verify.ts +0 -243
package/src/run/index.ts
CHANGED
|
@@ -18,28 +18,19 @@
|
|
|
18
18
|
// drive the CLI (which calls into the same core directly without MCP).
|
|
19
19
|
|
|
20
20
|
import { createHash } from 'node:crypto'
|
|
21
|
-
import { rpc
|
|
21
|
+
import { rpc } from '@stellar/stellar-sdk'
|
|
22
22
|
import {
|
|
23
23
|
type ErrorCode,
|
|
24
|
-
type MandateSpec,
|
|
25
24
|
type Network,
|
|
26
|
-
type OzAdapterConfig,
|
|
27
25
|
type PredicateNode,
|
|
28
26
|
type ProposedPolicy,
|
|
29
|
-
placeholderOzConfig,
|
|
30
27
|
type RecordedTransaction,
|
|
31
28
|
recordTransaction,
|
|
32
29
|
type SynthesizeFromRecordingOptions,
|
|
33
|
-
synthesizeFromMandate,
|
|
34
30
|
synthesizeFromRecording,
|
|
35
31
|
type ToolError,
|
|
36
32
|
type ToolResponse,
|
|
37
33
|
} from '../index.ts'
|
|
38
|
-
import {
|
|
39
|
-
type AuthorityOverlap,
|
|
40
|
-
type ContextType,
|
|
41
|
-
findAuthorityOverlaps,
|
|
42
|
-
} from '../install/authority-overlap.ts'
|
|
43
34
|
import {
|
|
44
35
|
type BuildInstallPolicyResult,
|
|
45
36
|
type BuildRevokePolicyResult,
|
|
@@ -48,29 +39,17 @@ import {
|
|
|
48
39
|
type InstallRpcClient,
|
|
49
40
|
rpcClientFromServer,
|
|
50
41
|
} from '../install/build-install-policy.ts'
|
|
51
|
-
import { buildMergePolicyXdr } from '../install/build-merge-policy.ts'
|
|
52
42
|
import { getInterpreterInfo } from '../install/get-interpreter-info.ts'
|
|
53
|
-
import {
|
|
54
|
-
import {
|
|
55
|
-
accountRuleReaderFromServer,
|
|
56
|
-
collectObservedRules,
|
|
57
|
-
nonceLedgerKey,
|
|
58
|
-
} from '../install/read-account-rules.ts'
|
|
59
|
-
import { decodePredicate } from '../predicate/decode.ts'
|
|
60
|
-
import { encodePredicate } from '../predicate/encode.ts'
|
|
61
|
-
import type { SimulationResult } from '../verify/envelope.ts'
|
|
62
|
-
import { simulatePolicy, verifyPolicy } from '../verify/index.ts'
|
|
43
|
+
import { type EvalContext, evaluate, generateCases } from '../simulate/index.ts'
|
|
63
44
|
import {
|
|
64
45
|
type GetInterpreterInfoInput,
|
|
65
46
|
GetInterpreterInfoInputSchema,
|
|
66
47
|
type InstallPolicyInput,
|
|
67
48
|
InstallPolicyInputSchema,
|
|
68
|
-
type MergePolicyInput,
|
|
69
|
-
MergePolicyInputSchema,
|
|
70
49
|
NETWORK_PASSPHRASES,
|
|
71
50
|
PINNED_INTERPRETER_ADDRESS_BY_NETWORK,
|
|
72
51
|
PINNED_INTERPRETER_GRAMMAR_VERSION,
|
|
73
|
-
|
|
52
|
+
PINNED_INTERPRETER_WASM_SHA256,
|
|
74
53
|
type RecordTransactionInput,
|
|
75
54
|
RecordTransactionInputSchema,
|
|
76
55
|
type RevokePolicyInput,
|
|
@@ -103,26 +82,21 @@ export {
|
|
|
103
82
|
InstallPolicyInputSchema,
|
|
104
83
|
InterpreterOptionsSchema,
|
|
105
84
|
MAINNET_RPC_URL,
|
|
106
|
-
MandateSpecSchema,
|
|
107
85
|
NetworkSchema,
|
|
108
|
-
OraclePriceFixtureSchema,
|
|
109
|
-
OzAdapterConfigSchema,
|
|
110
86
|
PINNED_INTERPRETER_ADDRESS_BY_NETWORK,
|
|
111
87
|
PINNED_INTERPRETER_GRAMMAR_VERSION,
|
|
112
88
|
PINNED_INTERPRETER_MAINNET_ADDRESS,
|
|
113
89
|
PINNED_INTERPRETER_TESTNET_ADDRESS,
|
|
114
|
-
|
|
90
|
+
PINNED_INTERPRETER_WASM_SHA256,
|
|
115
91
|
PredicateLeafSchema,
|
|
116
92
|
PredicateNodeSchema,
|
|
117
93
|
RecordedTransactionSchema,
|
|
118
94
|
RecordTransactionInputSchema,
|
|
119
95
|
RevokePolicyInputSchema,
|
|
120
96
|
RPC_URL_BY_NETWORK,
|
|
121
|
-
SimulatePolicyInputSchema,
|
|
122
97
|
SynthesizePolicyInputSchema,
|
|
123
98
|
TESTNET_RPC_URL,
|
|
124
99
|
ToolErrorSchema,
|
|
125
|
-
VerifyPolicyInputSchema,
|
|
126
100
|
} from './schemas.ts'
|
|
127
101
|
|
|
128
102
|
export type RunRecordTransactionInput = RecordTransactionInput
|
|
@@ -168,10 +142,7 @@ export async function runRecordTransaction(
|
|
|
168
142
|
): Promise<ToolResponse<RecordedTransaction>> {
|
|
169
143
|
const parsed = RecordTransactionInputSchema.safeParse(raw)
|
|
170
144
|
if (!parsed.success) {
|
|
171
|
-
return {
|
|
172
|
-
ok: false,
|
|
173
|
-
error: validationError('record_transaction', parsed.error.issues),
|
|
174
|
-
}
|
|
145
|
+
return { ok: false, error: validationError('record_transaction', parsed.error.issues) }
|
|
175
146
|
}
|
|
176
147
|
const input: RecordTransactionInput = parsed.data
|
|
177
148
|
// Strip the wire-only `confidenceOverride` and pass the rest straight through.
|
|
@@ -186,15 +157,11 @@ export async function runRecordTransaction(
|
|
|
186
157
|
try {
|
|
187
158
|
return await recordTransaction(coreInput)
|
|
188
159
|
} catch (e) {
|
|
189
|
-
return
|
|
190
|
-
ok: false,
|
|
191
|
-
error: caughtError('record_transaction', 'RECORDING_FAILED', e),
|
|
192
|
-
}
|
|
160
|
+
return toolFailure('record_transaction', e)
|
|
193
161
|
}
|
|
194
162
|
}
|
|
195
163
|
|
|
196
164
|
/** `synthesize_policy` body - discriminated union on `source`:
|
|
197
|
-
* - `mandate` -> synthesizeFromMandate
|
|
198
165
|
* - `recording` -> synthesizeFromRecording
|
|
199
166
|
* Exposing BOTH front-ends through ONE tool keeps the MCP surface tiny while
|
|
200
167
|
* letting the agent pick the deterministic or the inferred path. The CLI
|
|
@@ -209,155 +176,37 @@ export async function runSynthesizePolicy(raw: unknown): Promise<
|
|
|
209
176
|
ToolResponse<ProposedPolicy> & {
|
|
210
177
|
explain?: {
|
|
211
178
|
predicateTree: PredicateNode | null
|
|
212
|
-
simulation: SimulationResult
|
|
213
179
|
}
|
|
214
180
|
}
|
|
215
181
|
> {
|
|
216
182
|
const parsed = SynthesizePolicyInputSchema.safeParse(raw)
|
|
217
183
|
if (!parsed.success) {
|
|
218
|
-
return {
|
|
219
|
-
ok: false,
|
|
220
|
-
error: validationError('synthesize_policy', parsed.error.issues),
|
|
221
|
-
}
|
|
184
|
+
return { ok: false, error: validationError('synthesize_policy', parsed.error.issues) }
|
|
222
185
|
}
|
|
223
186
|
const input = parsed.data
|
|
224
|
-
const ozConfig: OzAdapterConfig = resolveOzConfig(input)
|
|
225
187
|
|
|
226
188
|
try {
|
|
227
|
-
if (input.source === 'mandate') {
|
|
228
|
-
// Zod's optional fields widen to `T | undefined`, which the core's
|
|
229
|
-
// exact-optional MandateSpec rejects; the schema already validated the
|
|
230
|
-
// shape, so assert it (same pattern as the recordedTx cast below).
|
|
231
|
-
return await synthesizeFromMandate(
|
|
232
|
-
input.mandate as MandateSpec,
|
|
233
|
-
ozConfig,
|
|
234
|
-
input.explain === true ? { explain: true } : {}
|
|
235
|
-
)
|
|
236
|
-
}
|
|
237
|
-
// recording source
|
|
238
189
|
const recorded: RecordedTransaction = input.recordedTx as RecordedTransaction
|
|
239
|
-
return await synthesizeFromRecording(
|
|
240
|
-
|
|
241
|
-
{
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
...(input.confidenceOverride !== undefined
|
|
245
|
-
? { confidenceOverride: input.confidenceOverride }
|
|
246
|
-
: {}),
|
|
247
|
-
...(input.interpreter !== undefined ? { interpreter: input.interpreter } : {}),
|
|
248
|
-
...(input.explain === true ? { explain: true } : {}),
|
|
249
|
-
} as SynthesizeFromRecordingOptions,
|
|
250
|
-
ozConfig
|
|
251
|
-
)
|
|
252
|
-
} catch (e) {
|
|
253
|
-
return {
|
|
254
|
-
ok: false,
|
|
255
|
-
error: caughtError('synthesize_policy', 'SYNTHESIS_ERROR', e),
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function resolveOzConfig(input: SynthesizePolicyInput): OzAdapterConfig {
|
|
261
|
-
if (input.ozConfig) return input.ozConfig
|
|
262
|
-
// The mandate path is network-agnostic; fall back to mainnet so the
|
|
263
|
-
// placeholder OZ instance addresses are deterministic.
|
|
264
|
-
return placeholderOzConfig('mainnet')
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/** `simulate_policy` body - thin wrapper over `simulatePolicy`. The engine
|
|
268
|
-
* already returns fail-closed `{ok:false, error}` for runtime failures
|
|
269
|
-
* (SIMULATION_ERROR), so the try/catch envelope is for raw SDK throws
|
|
270
|
-
* only - same pattern as the other two wrappers. The predicate is
|
|
271
|
-
* passed inline (stateless by design; no `proposed_policy_id` lookup). */
|
|
272
|
-
export async function runSimulatePolicy(raw: unknown): Promise<ToolResponse<SimulationResult>> {
|
|
273
|
-
const parsed = SimulatePolicyInputSchema.safeParse(raw)
|
|
274
|
-
if (!parsed.success) {
|
|
275
|
-
return {
|
|
276
|
-
ok: false,
|
|
277
|
-
error: validationError('simulate_policy', parsed.error.issues),
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
const input: SimulatePolicyInput = parsed.data
|
|
281
|
-
try {
|
|
282
|
-
// The recursive PredicateNodeSchema + ContractInvocationSchema are
|
|
283
|
-
// typed `z.ZodType<unknown>` to survive TS's circular inference; the
|
|
284
|
-
// engine wants typed `PredicateNode | null` + `RecordedTransaction`.
|
|
285
|
-
// The schema already validated the shape, so assert through the
|
|
286
|
-
// unknown back to the core types. Same pattern as the recordedTx
|
|
287
|
-
// cast in `runSynthesizePolicy`.
|
|
288
|
-
return simulatePolicy(
|
|
289
|
-
input.predicate as PredicateNode | null,
|
|
290
|
-
input.permitTx as RecordedTransaction,
|
|
291
|
-
{
|
|
292
|
-
...(input.validUntilLedger !== undefined
|
|
293
|
-
? { validUntilLedger: input.validUntilLedger }
|
|
294
|
-
: {}),
|
|
295
|
-
...(input.oraclePricesByAsset !== undefined
|
|
296
|
-
? { oraclePricesByAsset: input.oraclePricesByAsset }
|
|
297
|
-
: {}),
|
|
298
|
-
}
|
|
299
|
-
)
|
|
300
|
-
} catch (e) {
|
|
301
|
-
return {
|
|
302
|
-
ok: false,
|
|
303
|
-
error: caughtError('simulate_policy', 'SIMULATION_ERROR', e),
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/** `verify_policy` body - thin wrapper over `verifyPolicy`. The engine
|
|
309
|
-
* already returns `{ok:false, error}` with code VERIFICATION_FAILED when
|
|
310
|
-
* the minimality check fails; the try/catch envelope is for raw SDK
|
|
311
|
-
* throws only. Mirrors `runSimulatePolicy` exactly. */
|
|
312
|
-
export async function runVerifyPolicy(raw: unknown): Promise<ToolResponse<true>> {
|
|
313
|
-
const parsed = VerifyPolicyInputSchema.safeParse(raw)
|
|
314
|
-
if (!parsed.success) {
|
|
315
|
-
return {
|
|
316
|
-
ok: false,
|
|
317
|
-
error: validationError('verify_policy', parsed.error.issues),
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
const input: VerifyPolicyInput = parsed.data
|
|
321
|
-
try {
|
|
322
|
-
// Same cast as runSimulatePolicy: the recursive schemas are typed
|
|
323
|
-
// `unknown`; the engine wants typed `PredicateNode` +
|
|
324
|
-
// `RecordedTransaction`. The schema already validated the shape.
|
|
325
|
-
return verifyPolicy(input.predicate as PredicateNode, input.permitTx as RecordedTransaction, {
|
|
326
|
-
...(input.validUntilLedger !== undefined ? { validUntilLedger: input.validUntilLedger } : {}),
|
|
327
|
-
...(input.oraclePricesByAsset !== undefined
|
|
328
|
-
? { oraclePricesByAsset: input.oraclePricesByAsset }
|
|
190
|
+
return await synthesizeFromRecording(recorded, {
|
|
191
|
+
network: input.network,
|
|
192
|
+
...(input.userResponses !== undefined ? { userResponses: input.userResponses } : {}),
|
|
193
|
+
...(input.confidenceOverride !== undefined
|
|
194
|
+
? { confidenceOverride: input.confidenceOverride }
|
|
329
195
|
: {}),
|
|
330
|
-
|
|
196
|
+
...(input.interpreter !== undefined ? { interpreter: input.interpreter } : {}),
|
|
197
|
+
...(input.explain === true ? { explain: true } : {}),
|
|
198
|
+
} as SynthesizeFromRecordingOptions)
|
|
331
199
|
} catch (e) {
|
|
332
|
-
return
|
|
333
|
-
ok: false,
|
|
334
|
-
error: caughtError('verify_policy', 'VERIFICATION_FAILED', e),
|
|
335
|
-
}
|
|
200
|
+
return toolFailure('synthesize_policy', e)
|
|
336
201
|
}
|
|
337
202
|
}
|
|
338
203
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
* `action_id` two-call pair (the server is stateless, see server.ts:10-12).
|
|
343
|
-
* One call installs the policy outright: `add_context_rule` carries the
|
|
344
|
-
* predicate to the interpreter in its `policies` install_param, so no
|
|
345
|
-
* separate `interpreter.install` call is needed or possible.
|
|
346
|
-
*
|
|
347
|
-
* Default-deny: an interpreter policy address other than the pinned
|
|
348
|
-
* interpreter for the selected network is REFUSED (the smart account
|
|
349
|
-
* would delegate to an interpreter the caller controls); the same
|
|
350
|
-
* applies to a non-pinned RPC URL (the auth nonce the wallet signs
|
|
351
|
-
* comes from the RPC). Both gates accept an explicit opt-in flag.
|
|
352
|
-
* Pin selection follows `input.network` (defaults to `testnet` so the
|
|
353
|
-
* pre-mainnet callers keep working unchanged). */
|
|
354
|
-
export async function runInstallPolicy(raw: unknown): Promise<ToolResponse<InstallPolicyResult>> {
|
|
204
|
+
export async function runInstallPolicy(
|
|
205
|
+
raw: unknown
|
|
206
|
+
): Promise<ToolResponse<BuildInstallPolicyResult>> {
|
|
355
207
|
const parsed = InstallPolicyInputSchema.safeParse(raw)
|
|
356
208
|
if (!parsed.success) {
|
|
357
|
-
return {
|
|
358
|
-
ok: false,
|
|
359
|
-
error: validationError('install_policy', parsed.error.issues),
|
|
360
|
-
}
|
|
209
|
+
return { ok: false, error: validationError('install_policy', parsed.error.issues) }
|
|
361
210
|
}
|
|
362
211
|
const input: InstallPolicyInput = parsed.data
|
|
363
212
|
const network: Network = input.network ?? 'testnet'
|
|
@@ -386,10 +235,7 @@ export async function runInstallPolicy(raw: unknown): Promise<ToolResponse<Insta
|
|
|
386
235
|
try {
|
|
387
236
|
rpcClient = buildRpcClientFromInput(input.rpcUrl, network)
|
|
388
237
|
} catch (e) {
|
|
389
|
-
return
|
|
390
|
-
ok: false,
|
|
391
|
-
error: caughtError('install_policy', 'INSTALL_BUILD_FAILED', e),
|
|
392
|
-
}
|
|
238
|
+
return toolFailure('install_policy', e)
|
|
393
239
|
}
|
|
394
240
|
try {
|
|
395
241
|
const interpreterPolicy = input.rule.policies.find((p) => p.kind === 'interpreter')
|
|
@@ -397,27 +243,6 @@ export async function runInstallPolicy(raw: unknown): Promise<ToolResponse<Insta
|
|
|
397
243
|
const predicateHash = createHash('sha256')
|
|
398
244
|
.update(Buffer.from(encodedPredicate, 'base64'))
|
|
399
245
|
.digest('hex')
|
|
400
|
-
|
|
401
|
-
// ---- Cross-rule authority scan ----
|
|
402
|
-
// OZ enforces only the policies of the rule the caller names, so a signer
|
|
403
|
-
// who also sits in a wider rule keeps that wider authority no matter what
|
|
404
|
-
// this predicate says. Refuse by default when the wider rule has no policy
|
|
405
|
-
// at all, because that makes this install decorative.
|
|
406
|
-
const authorityScan: AuthorityScanReport | undefined = input.skipAuthorityScan
|
|
407
|
-
? { ran: false, skipped: true, reason: 'skipped at caller request', overlaps: [] }
|
|
408
|
-
: await scanAuthorityOverlap({
|
|
409
|
-
smartAccount: input.smartAccount,
|
|
410
|
-
interpreterAddress: expectedInterpreter,
|
|
411
|
-
rule: input.rule,
|
|
412
|
-
encodedPredicate,
|
|
413
|
-
rpcUrl: input.rpcUrl ?? expectedRpc,
|
|
414
|
-
network,
|
|
415
|
-
})
|
|
416
|
-
const overlapError = enforceAuthorityScan(authorityScan, input.allowAuthorityOverlap)
|
|
417
|
-
if (overlapError) {
|
|
418
|
-
return { ok: false, error: overlapError }
|
|
419
|
-
}
|
|
420
|
-
|
|
421
246
|
const result = await buildInstallPolicyXdr({
|
|
422
247
|
smartAccount: input.smartAccount,
|
|
423
248
|
sourceAccount: input.sourceAccount,
|
|
@@ -429,349 +254,9 @@ export async function runInstallPolicy(raw: unknown): Promise<ToolResponse<Insta
|
|
|
429
254
|
rpc: rpcClient,
|
|
430
255
|
...(input.baseFee !== undefined ? { baseFee: input.baseFee } : {}),
|
|
431
256
|
})
|
|
432
|
-
return { ok: true, data:
|
|
433
|
-
} catch (e) {
|
|
434
|
-
return {
|
|
435
|
-
ok: false,
|
|
436
|
-
error: caughtError('install_policy', 'INSTALL_BUILD_FAILED', e),
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/** Default-deny on the cross-rule scan.
|
|
442
|
-
*
|
|
443
|
-
* Refuses whenever the scan cannot establish that this policy binds the calls
|
|
444
|
-
* it names. An unpoliced neighbour provably does not constrain them. An
|
|
445
|
-
* opaque one, policed by a contract this tool cannot decode, is not KNOWN to,
|
|
446
|
-
* and "not known to" is not "safe" - the same posture as the interpreter and
|
|
447
|
-
* RPC pins. An incomplete scan is refused for the same reason: the overlap
|
|
448
|
-
* list is then a subset of the account, so an empty list proves nothing.
|
|
449
|
-
*
|
|
450
|
-
* `not-restricting` is reported but does NOT block. Both rules are ours and
|
|
451
|
-
* both constrain the calls, and the conjunction remedy is offered; refusing
|
|
452
|
-
* there would also block the legitimate act of adding a separate capability,
|
|
453
|
-
* which OZ composes correctly as a union.
|
|
454
|
-
*
|
|
455
|
-
* Returns a ToolError or null, matching `enforceInterpreterPin`. */
|
|
456
|
-
export function enforceAuthorityScan(
|
|
457
|
-
scan: AuthorityScanReport | undefined,
|
|
458
|
-
allowOverlap: boolean | undefined
|
|
459
|
-
): ToolError | null {
|
|
460
|
-
if (!scan || allowOverlap === true) return null
|
|
461
|
-
|
|
462
|
-
// Default-deny by exclusion rather than by enumeration: anything that is not
|
|
463
|
-
// the one known-safe severity blocks. Listing the blocking severities
|
|
464
|
-
// instead would mean a severity added later silently passes until someone
|
|
465
|
-
// remembers to add it here, and the safe direction is the opposite.
|
|
466
|
-
const blocking = scan.overlaps.filter((o) => o.severity !== 'not-restricting')
|
|
467
|
-
const unpoliced = blocking.filter((o) => o.severity === 'bypass').map((o) => o.ruleId)
|
|
468
|
-
const opaque = blocking.filter((o) => o.severity === 'unknown').map((o) => o.ruleId)
|
|
469
|
-
const unrecognised = blocking
|
|
470
|
-
.filter((o) => o.severity !== 'bypass' && o.severity !== 'unknown')
|
|
471
|
-
.map((o) => o.ruleId)
|
|
472
|
-
|
|
473
|
-
if (blocking.length > 0) {
|
|
474
|
-
const parts = [
|
|
475
|
-
unpoliced.length > 0 ? `rule ${unpoliced.join(', ')} has no policy attached` : '',
|
|
476
|
-
opaque.length > 0
|
|
477
|
-
? `rule ${opaque.join(', ')} is policed by a contract this tool cannot decode`
|
|
478
|
-
: '',
|
|
479
|
-
unrecognised.length > 0
|
|
480
|
-
? `rule ${unrecognised.join(', ')} carries an overlap this build does not recognise`
|
|
481
|
-
: '',
|
|
482
|
-
].filter(Boolean)
|
|
483
|
-
return {
|
|
484
|
-
code: 'INSTALL_BUILD_FAILED',
|
|
485
|
-
message: `install_policy: a signer of this rule can already make the same calls through another context rule, so this policy is not established to restrict them: ${parts.join('; ')}. Remove the shared signer from that rule, attach a policy this tool can read, or set allowAuthorityOverlap: true to install anyway`,
|
|
486
|
-
severity: 'error',
|
|
487
|
-
retryable: false,
|
|
488
|
-
remediation: { toolCall: { name: 'install_policy', args: {} } },
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// A scan that was meant to run and threw tells us nothing. Reporting it in
|
|
493
|
-
// a string field and proceeding is the same fail-open shape as an incomplete
|
|
494
|
-
// scan, and a caller that does not read `ran` cannot tell it from a clean
|
|
495
|
-
// result.
|
|
496
|
-
if (scan.ran === false && scan.skipped !== true) {
|
|
497
|
-
return {
|
|
498
|
-
code: 'INSTALL_BUILD_FAILED',
|
|
499
|
-
message: `install_policy: the cross-rule authority scan could not run (${scan.reason ?? 'unknown error'}), so it cannot establish that this policy restricts anything; retry, or set allowAuthorityOverlap: true to install without that assurance`,
|
|
500
|
-
severity: 'error',
|
|
501
|
-
retryable: true,
|
|
502
|
-
remediation: { toolCall: { name: 'install_policy', args: {} } },
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
if (scan.incomplete === true) {
|
|
507
|
-
return {
|
|
508
|
-
code: 'INSTALL_BUILD_FAILED',
|
|
509
|
-
message:
|
|
510
|
-
'install_policy: the account has more context rules than the scan could account for, so the overlap result is incomplete and cannot establish that this policy restricts anything; set allowAuthorityOverlap: true to install without that assurance',
|
|
511
|
-
severity: 'error',
|
|
512
|
-
retryable: false,
|
|
513
|
-
remediation: { toolCall: { name: 'install_policy', args: {} } },
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
return null
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
/** The install response, plus what the cross-rule scan found. The scan is
|
|
521
|
-
* advisory data about the account, not part of the transaction, so it is
|
|
522
|
-
* additive: a caller that ignores it gets exactly the previous shape. */
|
|
523
|
-
export type InstallPolicyResult = BuildInstallPolicyResult & {
|
|
524
|
-
authorityScan?: AuthorityScanReport
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
/** What the cross-rule scan found, carried on the install response so the
|
|
528
|
-
* review surface can show it alongside the transaction being signed.
|
|
529
|
-
*
|
|
530
|
-
* SCOPE, and it is narrow: this answers "can a signer OF THIS RULE reach the
|
|
531
|
-
* same calls through a different rule". A rule sharing no signer with this
|
|
532
|
-
* one cannot be reached by this rule's signers, so it is not a way around
|
|
533
|
-
* this policy; it is a different principal's authority, which no policy
|
|
534
|
-
* installed here was ever going to constrain. Other rules keep their own
|
|
535
|
-
* signers, and an account administrator can add signers or rules afterwards.
|
|
536
|
-
*
|
|
537
|
-
* An empty `overlaps` is therefore NOT a statement that the account is safe,
|
|
538
|
-
* only that this rule's own signers gain no unconstrained path through the
|
|
539
|
-
* rules that exist right now. */
|
|
540
|
-
export interface AuthorityScanReport {
|
|
541
|
-
/** False when the scan did not run. `reason` then says why, and the absence
|
|
542
|
-
* of overlaps proves nothing. */
|
|
543
|
-
ran: boolean
|
|
544
|
-
/** True when the caller passed `skipAuthorityScan`. Distinguishes a
|
|
545
|
-
* deliberate skip from a scan that tried and failed: both carry
|
|
546
|
-
* `ran: false`, but only the failure refuses the install. Recorded rather
|
|
547
|
-
* than omitted so the response shows that no opinion was formed, instead
|
|
548
|
-
* of looking like a version that never had the check. */
|
|
549
|
-
skipped?: boolean
|
|
550
|
-
/** True when the account has more rules than the scan accounted for, so the
|
|
551
|
-
* overlap list is a subset. */
|
|
552
|
-
incomplete?: boolean
|
|
553
|
-
reason?: string
|
|
554
|
-
overlaps: AuthorityOverlap[]
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
/** Read the account's other context rules and report where this install's
|
|
558
|
-
* signers already hold authority over the same calls.
|
|
559
|
-
*
|
|
560
|
-
* A failure to read is reported rather than thrown, and the caller-facing
|
|
561
|
-
* decision is made by `enforceAuthorityScan`: a scan that tried and failed
|
|
562
|
-
* tells us nothing, so it refuses rather than passing as a clean account.
|
|
563
|
-
*
|
|
564
|
-
* The account data this trusts comes from whichever RPC answered, so a
|
|
565
|
-
* hostile RPC could describe an account with no overlapping rules. That is
|
|
566
|
-
* bounded by the pin already enforced above: `enforceRpcPin` returns before
|
|
567
|
-
* this runs, so the URL is the pinned one for the network unless the caller
|
|
568
|
-
* explicitly set `allowUnpinnedRpcUrl`. This scan deliberately does not add a
|
|
569
|
-
* second pin check, because two places deciding the same thing drift. */
|
|
570
|
-
async function scanAuthorityOverlap(args: {
|
|
571
|
-
smartAccount: string
|
|
572
|
-
interpreterAddress: string
|
|
573
|
-
rule: InstallPolicyInput['rule']
|
|
574
|
-
encodedPredicate: string
|
|
575
|
-
rpcUrl: string
|
|
576
|
-
network: Network
|
|
577
|
-
}): Promise<AuthorityScanReport | undefined> {
|
|
578
|
-
try {
|
|
579
|
-
if (!args.encodedPredicate) {
|
|
580
|
-
// Not applicable rather than failed: this rule installs no interpreter
|
|
581
|
-
// predicate, so there is nothing of ours for another rule to undercut.
|
|
582
|
-
// Returning undefined keeps it out of the refusal path, which is
|
|
583
|
-
// reserved for scans that were meant to run and could not.
|
|
584
|
-
return undefined
|
|
585
|
-
}
|
|
586
|
-
const server = new rpc.Server(args.rpcUrl, { allowHttp: false })
|
|
587
|
-
const reader = accountRuleReaderFromServer(server, NETWORK_PASSPHRASES[args.network])
|
|
588
|
-
const collected = await collectObservedRules({
|
|
589
|
-
reader,
|
|
590
|
-
smartAccount: args.smartAccount,
|
|
591
|
-
interpreterAddress: args.interpreterAddress,
|
|
592
|
-
})
|
|
593
|
-
const overlaps = findAuthorityOverlaps({
|
|
594
|
-
intended: {
|
|
595
|
-
// `add_context_rule` always creates a NEW rule, so there is no id to
|
|
596
|
-
// exclude; -1 matches nothing on the account.
|
|
597
|
-
ruleId: -1,
|
|
598
|
-
contextType: toOverlapContextType(args.rule.contextRuleType),
|
|
599
|
-
signers: args.rule.signers,
|
|
600
|
-
predicate: decodePredicate(args.encodedPredicate),
|
|
601
|
-
},
|
|
602
|
-
existing: collected.rules,
|
|
603
|
-
})
|
|
604
|
-
return { ran: true, incomplete: collected.incomplete, overlaps }
|
|
605
|
-
} catch (e) {
|
|
606
|
-
return {
|
|
607
|
-
ran: false,
|
|
608
|
-
reason: e instanceof Error ? e.message : String(e),
|
|
609
|
-
overlaps: [],
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
/** The rule draft names the callee `contract`; the analyser calls it
|
|
615
|
-
* `address`. Same value, two vocabularies. */
|
|
616
|
-
function toOverlapContextType(ct: InstallPolicyInput['rule']['contextRuleType']): ContextType {
|
|
617
|
-
switch (ct.kind) {
|
|
618
|
-
case 'call_contract':
|
|
619
|
-
return { kind: 'call_contract', address: ct.contract }
|
|
620
|
-
case 'create_contract':
|
|
621
|
-
return { kind: 'create_contract', wasmHash: ct.wasmHash }
|
|
622
|
-
default:
|
|
623
|
-
return { kind: 'default' }
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
/** The merge response: one step's transaction plus what it will cost. */
|
|
628
|
-
export interface MergePolicyResult {
|
|
629
|
-
unsignedXdr: string
|
|
630
|
-
smartAccount: string
|
|
631
|
-
sourceAccount: string
|
|
632
|
-
step: 'detach' | 'reinstall'
|
|
633
|
-
call: { contract: string; fn: string; ruleId: number }
|
|
634
|
-
authNonce: string
|
|
635
|
-
authValidUntilLedger: number
|
|
636
|
-
rootInvocationXdr: string
|
|
637
|
-
/** sha256 of the merged predicate, so the caller can pin what step 2 will
|
|
638
|
-
* install while they are still looking at step 1. */
|
|
639
|
-
mergedPredicateHash: string
|
|
640
|
-
mergedPredicateBlobBase64: string
|
|
641
|
-
warnings: string[]
|
|
642
|
-
followUp: string
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
/** `merge_policy` body - the tightening remedy for a cross-rule overlap.
|
|
646
|
-
*
|
|
647
|
-
* Replaces a rule's predicate with the conjunction of it and a new one. This
|
|
648
|
-
* is the action `install_policy` recommends when it reports an overlap
|
|
649
|
-
* between two rules our interpreter polices, and it is deliberately NOT
|
|
650
|
-
* something `install_policy` does on its own: it detaches a live policy, so
|
|
651
|
-
* the operator has to ask for it.
|
|
652
|
-
*
|
|
653
|
-
* Two transactions in order. `add_policy` refuses a policy already on the
|
|
654
|
-
* rule, so the old attachment goes first, and the second transaction cannot
|
|
655
|
-
* be simulated until the first confirms. */
|
|
656
|
-
export async function runMergePolicy(raw: unknown): Promise<ToolResponse<MergePolicyResult>> {
|
|
657
|
-
const parsed = MergePolicyInputSchema.safeParse(raw)
|
|
658
|
-
if (!parsed.success) {
|
|
659
|
-
return { ok: false, error: validationError('install_policy', parsed.error.issues) }
|
|
660
|
-
}
|
|
661
|
-
const input: MergePolicyInput = parsed.data
|
|
662
|
-
const network: Network = input.network ?? 'testnet'
|
|
663
|
-
const expectedInterpreter = PINNED_INTERPRETER_ADDRESS_BY_NETWORK[network]
|
|
664
|
-
const expectedRpc = RPC_URL_BY_NETWORK[network]
|
|
665
|
-
|
|
666
|
-
const rpcPinningError = enforceRpcPin(
|
|
667
|
-
'install_policy',
|
|
668
|
-
input.rpcUrl,
|
|
669
|
-
input.allowUnpinnedRpcUrl,
|
|
670
|
-
expectedRpc,
|
|
671
|
-
network
|
|
672
|
-
)
|
|
673
|
-
if (rpcPinningError) return { ok: false, error: rpcPinningError }
|
|
674
|
-
|
|
675
|
-
try {
|
|
676
|
-
const rpcUrl = input.rpcUrl ?? expectedRpc
|
|
677
|
-
const server = new rpc.Server(rpcUrl, { allowHttp: false })
|
|
678
|
-
const reader = accountRuleReaderFromServer(server, NETWORK_PASSPHRASES[network])
|
|
679
|
-
const collected = await collectObservedRules({
|
|
680
|
-
reader,
|
|
681
|
-
smartAccount: input.smartAccount,
|
|
682
|
-
interpreterAddress: expectedInterpreter,
|
|
683
|
-
})
|
|
684
|
-
const rule = collected.rules.find((r) => r.id === input.ruleId)
|
|
685
|
-
if (!rule) {
|
|
686
|
-
return {
|
|
687
|
-
ok: false,
|
|
688
|
-
error: {
|
|
689
|
-
code: 'INSTALL_BUILD_FAILED',
|
|
690
|
-
message: `merge_policy: rule ${input.ruleId} was not found on ${input.smartAccount}${
|
|
691
|
-
collected.incomplete
|
|
692
|
-
? ' (the rule scan was incomplete, so it may exist but was not reached)'
|
|
693
|
-
: ''
|
|
694
|
-
}`,
|
|
695
|
-
severity: 'error',
|
|
696
|
-
retryable: false,
|
|
697
|
-
remediation: { toolCall: { name: 'install_policy', args: {} } },
|
|
698
|
-
},
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
const plan = planMergePolicy({
|
|
703
|
-
rule,
|
|
704
|
-
interpreterAddress: expectedInterpreter,
|
|
705
|
-
incoming: decodePredicate(input.incomingPredicateBlobBase64),
|
|
706
|
-
step: input.step,
|
|
707
|
-
})
|
|
708
|
-
if (!plan.ok) {
|
|
709
|
-
return {
|
|
710
|
-
ok: false,
|
|
711
|
-
error: {
|
|
712
|
-
code: 'INSTALL_BUILD_FAILED',
|
|
713
|
-
message: `merge_policy: ${plan.reason}`,
|
|
714
|
-
severity: 'error',
|
|
715
|
-
retryable: false,
|
|
716
|
-
remediation: { toolCall: { name: 'install_policy', args: {} } },
|
|
717
|
-
},
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
const encoded = encodePredicate(plan.predicate)
|
|
722
|
-
|
|
723
|
-
// The nonce is read, not assumed. OZ's `remove_policy` discards the result
|
|
724
|
-
// of `try_uninstall`, so a detach whose uninstall panicked - our
|
|
725
|
-
// `uninstall` panics MissingState when the master set has been archived -
|
|
726
|
-
// detaches the policy while leaving our nonce behind. Re-installing at 1
|
|
727
|
-
// would then be refused as a replay and the rule would sit unpoliced.
|
|
728
|
-
let installNonce = 1
|
|
729
|
-
const nonceWarnings: string[] = []
|
|
730
|
-
if (input.step === 'reinstall') {
|
|
731
|
-
const entries = await server.getLedgerEntries(
|
|
732
|
-
nonceLedgerKey(expectedInterpreter, input.smartAccount, input.ruleId)
|
|
733
|
-
)
|
|
734
|
-
const raw = entries.entries?.[0]?.val
|
|
735
|
-
const stored =
|
|
736
|
-
raw && raw.switch() === xdr.LedgerEntryType.contractData()
|
|
737
|
-
? raw.contractData().val()
|
|
738
|
-
: undefined
|
|
739
|
-
if (stored && stored.switch() === xdr.ScValType.scvU32()) {
|
|
740
|
-
installNonce = stored.u32() + 1
|
|
741
|
-
nonceWarnings.push(
|
|
742
|
-
`the previous uninstall did not complete: rule ${input.ruleId} still holds interpreter state at nonce ${stored.u32()}, so this reinstalls at ${installNonce} rather than 1. The rule's counters were NOT reset.`
|
|
743
|
-
)
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
const built = await buildMergePolicyXdr({
|
|
748
|
-
smartAccount: input.smartAccount,
|
|
749
|
-
sourceAccount: input.sourceAccount,
|
|
750
|
-
networkPassphrase: NETWORK_PASSPHRASES[network],
|
|
751
|
-
ruleId: input.ruleId,
|
|
752
|
-
policyId: plan.policyId,
|
|
753
|
-
interpreterAddress: expectedInterpreter,
|
|
754
|
-
step: input.step,
|
|
755
|
-
encodedPredicate: encoded.encodedPredicate,
|
|
756
|
-
predicateHash: encoded.predicateHash,
|
|
757
|
-
installNonce,
|
|
758
|
-
...(plan.oracleParams ? { oracleParams: plan.oracleParams } : {}),
|
|
759
|
-
rpc: rpcClientFromServer(server, NETWORK_PASSPHRASES[network]),
|
|
760
|
-
...(input.baseFee !== undefined ? { baseFee: input.baseFee } : {}),
|
|
761
|
-
})
|
|
762
|
-
|
|
763
|
-
return {
|
|
764
|
-
ok: true,
|
|
765
|
-
data: {
|
|
766
|
-
...built,
|
|
767
|
-
mergedPredicateHash: encoded.predicateHash,
|
|
768
|
-
mergedPredicateBlobBase64: encoded.encodedPredicate,
|
|
769
|
-
warnings: [...plan.warnings, ...nonceWarnings],
|
|
770
|
-
followUp: plan.followUp,
|
|
771
|
-
},
|
|
772
|
-
}
|
|
257
|
+
return { ok: true, data: result }
|
|
773
258
|
} catch (e) {
|
|
774
|
-
return
|
|
259
|
+
return toolFailure('install_policy', e)
|
|
775
260
|
}
|
|
776
261
|
}
|
|
777
262
|
|
|
@@ -789,10 +274,7 @@ export async function runRevokePolicy(
|
|
|
789
274
|
): Promise<ToolResponse<BuildRevokePolicyResult>> {
|
|
790
275
|
const parsed = RevokePolicyInputSchema.safeParse(raw)
|
|
791
276
|
if (!parsed.success) {
|
|
792
|
-
return {
|
|
793
|
-
ok: false,
|
|
794
|
-
error: validationError('revoke_policy', parsed.error.issues),
|
|
795
|
-
}
|
|
277
|
+
return { ok: false, error: validationError('revoke_policy', parsed.error.issues) }
|
|
796
278
|
}
|
|
797
279
|
const input: RevokePolicyInput = parsed.data
|
|
798
280
|
const network: Network = input.network ?? 'testnet'
|
|
@@ -811,10 +293,7 @@ export async function runRevokePolicy(
|
|
|
811
293
|
try {
|
|
812
294
|
rpcClient = buildRpcClientFromInput(input.rpcUrl, network)
|
|
813
295
|
} catch (e) {
|
|
814
|
-
return
|
|
815
|
-
ok: false,
|
|
816
|
-
error: caughtError('revoke_policy', 'REVOKE_BUILD_FAILED', e),
|
|
817
|
-
}
|
|
296
|
+
return toolFailure('revoke_policy', e)
|
|
818
297
|
}
|
|
819
298
|
try {
|
|
820
299
|
const result = await buildRevokePolicyXdr({
|
|
@@ -827,10 +306,7 @@ export async function runRevokePolicy(
|
|
|
827
306
|
})
|
|
828
307
|
return { ok: true, data: result }
|
|
829
308
|
} catch (e) {
|
|
830
|
-
return
|
|
831
|
-
ok: false,
|
|
832
|
-
error: caughtError('revoke_policy', 'REVOKE_BUILD_FAILED', e),
|
|
833
|
-
}
|
|
309
|
+
return toolFailure('revoke_policy', e)
|
|
834
310
|
}
|
|
835
311
|
}
|
|
836
312
|
|
|
@@ -841,27 +317,126 @@ export async function runRevokePolicy(
|
|
|
841
317
|
* fabricating it would be a lie on a security surface; the live
|
|
842
318
|
* mismatch check is worth MORE).
|
|
843
319
|
*
|
|
844
|
-
* Network-aware: `input.network` selects
|
|
845
|
-
*
|
|
846
|
-
*
|
|
847
|
-
*
|
|
848
|
-
*
|
|
849
|
-
* writing.
|
|
320
|
+
* Network-aware: `input.network` selects which interpreter pin and RPC
|
|
321
|
+
* to use. Mainnet was rolled out 2026-08-04 - the same wasm hash was
|
|
322
|
+
* uploaded to mainnet as was exercised on testnet, so a single
|
|
323
|
+
* `PINNED_INTERPRETER_WASM_SHA256` constant backs both networks.
|
|
324
|
+
* The address differs because instance ids are network-scoped.
|
|
325
|
+
* UNAUDITED at the time of writing.
|
|
850
326
|
*
|
|
851
327
|
* Same RPC pin as install/revoke: when `verifyLive` triggers an outbound
|
|
852
328
|
* call, the auth-digest + the answer bind to whichever RPC answered, so
|
|
853
329
|
* a non-pinned `rpcUrl` would silently bind the caller to a host they
|
|
854
330
|
* picked. The pin is enforced here too, with the same `allowUnpinnedRpcUrl`
|
|
855
331
|
* opt-in as install/revoke. */
|
|
332
|
+
/** The call a predicate is evaluated against: the single top-level invocation
|
|
333
|
+
* the smart account authorises. `Policy::enforce` receives one `Context`, not
|
|
334
|
+
* a sub-invocation tree, so simulating anything deeper would claim a
|
|
335
|
+
* guarantee the contract does not make. */
|
|
336
|
+
function evalContextFromRecording(tx: RecordedTransaction): EvalContext | null {
|
|
337
|
+
const top = tx.invocations[0]
|
|
338
|
+
if (!top) return null
|
|
339
|
+
return { contract: top.contract, fn: top.fn, args: top.args }
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Both `simulate_policy` and `verify_policy` evaluate against a context derived
|
|
343
|
+
* from `permitTx`; neither has anything to evaluate when the recording carries
|
|
344
|
+
* no top-level invocation. */
|
|
345
|
+
function noInvocationError(toolName: 'simulate_policy' | 'verify_policy'): ToolError {
|
|
346
|
+
return {
|
|
347
|
+
code: TOOL_ERROR_CODE[toolName],
|
|
348
|
+
message: `${toolName}: permitTx carries no invocation to evaluate`,
|
|
349
|
+
severity: 'error',
|
|
350
|
+
retryable: false,
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** `simulate_policy` body - evaluate a predicate against one recorded call.
|
|
355
|
+
*
|
|
356
|
+
* The evaluator is a second implementation of the on-chain semantics, and the
|
|
357
|
+
* conformance harness asserts it agrees with the Rust interpreter case for
|
|
358
|
+
* case. A verdict here is therefore a claim about what the contract would do,
|
|
359
|
+
* not a guess. */
|
|
360
|
+
export function runSimulatePolicy(raw: unknown): ToolResponse<{
|
|
361
|
+
permitted: boolean
|
|
362
|
+
reason: string | null
|
|
363
|
+
call: { contract: string; fn: string; argCount: number }
|
|
364
|
+
}> {
|
|
365
|
+
const parsed = SimulatePolicyInputSchema.safeParse(raw)
|
|
366
|
+
if (!parsed.success) {
|
|
367
|
+
return { ok: false, error: validationError('simulate_policy', parsed.error.issues) }
|
|
368
|
+
}
|
|
369
|
+
const input: SimulatePolicyInput = parsed.data
|
|
370
|
+
const ctx = evalContextFromRecording(input.permitTx as RecordedTransaction)
|
|
371
|
+
if (!ctx) return { ok: false, error: noInvocationError('simulate_policy') }
|
|
372
|
+
try {
|
|
373
|
+
const res = evaluate(input.predicate as PredicateNode, ctx)
|
|
374
|
+
return {
|
|
375
|
+
ok: true,
|
|
376
|
+
data: {
|
|
377
|
+
permitted: res.permit,
|
|
378
|
+
reason: res.permit ? null : res.reason,
|
|
379
|
+
call: { contract: ctx.contract, fn: ctx.fn, argCount: ctx.args.length },
|
|
380
|
+
},
|
|
381
|
+
}
|
|
382
|
+
} catch (e) {
|
|
383
|
+
return toolFailure('simulate_policy', e)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** `verify_policy` body - the permit case plus a generated deny case per
|
|
388
|
+
* dimension.
|
|
389
|
+
*
|
|
390
|
+
* Two failure modes are being checked, and they are not the same thing. A
|
|
391
|
+
* permit case that denies means the policy is too STRICT: it would refuse the
|
|
392
|
+
* very transaction it was synthesised from. A deny case that permits means it
|
|
393
|
+
* is too LOOSE: some mutation of that transaction still gets through. `ok` is
|
|
394
|
+
* true only when neither holds. */
|
|
395
|
+
export function runVerifyPolicy(raw: unknown): ToolResponse<{
|
|
396
|
+
ok: boolean
|
|
397
|
+
permit: { permitted: boolean; reason: string | null }
|
|
398
|
+
denies: Array<{ dimension: string; denied: boolean; reason: string | null }>
|
|
399
|
+
dimensionsCovered: number
|
|
400
|
+
}> {
|
|
401
|
+
const parsed = VerifyPolicyInputSchema.safeParse(raw)
|
|
402
|
+
if (!parsed.success) {
|
|
403
|
+
return { ok: false, error: validationError('verify_policy', parsed.error.issues) }
|
|
404
|
+
}
|
|
405
|
+
const input: VerifyPolicyInput = parsed.data
|
|
406
|
+
const ctx = evalContextFromRecording(input.permitTx as RecordedTransaction)
|
|
407
|
+
if (!ctx) return { ok: false, error: noInvocationError('verify_policy') }
|
|
408
|
+
try {
|
|
409
|
+
const predicate = input.predicate as PredicateNode
|
|
410
|
+
const cases = generateCases(predicate, ctx)
|
|
411
|
+
const permitRes = evaluate(predicate, cases.permit)
|
|
412
|
+
const denies = cases.denies.map((d) => {
|
|
413
|
+
const r = evaluate(predicate, d.ctx)
|
|
414
|
+
return {
|
|
415
|
+
dimension: d.dimension,
|
|
416
|
+
denied: !r.permit,
|
|
417
|
+
reason: r.permit ? null : r.reason,
|
|
418
|
+
}
|
|
419
|
+
})
|
|
420
|
+
return {
|
|
421
|
+
ok: true,
|
|
422
|
+
data: {
|
|
423
|
+
ok: permitRes.permit && denies.every((d) => d.denied),
|
|
424
|
+
permit: { permitted: permitRes.permit, reason: permitRes.permit ? null : permitRes.reason },
|
|
425
|
+
denies,
|
|
426
|
+
dimensionsCovered: denies.length,
|
|
427
|
+
},
|
|
428
|
+
}
|
|
429
|
+
} catch (e) {
|
|
430
|
+
return toolFailure('verify_policy', e)
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
856
434
|
export async function runGetInterpreterInfo(
|
|
857
435
|
raw: unknown
|
|
858
436
|
): Promise<ToolResponse<ReturnType<typeof getInterpreterInfo>>> {
|
|
859
437
|
const parsed = GetInterpreterInfoInputSchema.safeParse(raw)
|
|
860
438
|
if (!parsed.success) {
|
|
861
|
-
return {
|
|
862
|
-
ok: false,
|
|
863
|
-
error: validationError('get_interpreter_info', parsed.error.issues),
|
|
864
|
-
}
|
|
439
|
+
return { ok: false, error: validationError('get_interpreter_info', parsed.error.issues) }
|
|
865
440
|
}
|
|
866
441
|
const input: GetInterpreterInfoInput = parsed.data
|
|
867
442
|
const network: Network = input.network ?? 'testnet'
|
|
@@ -894,16 +469,13 @@ export async function runGetInterpreterInfo(
|
|
|
894
469
|
const info = getInterpreterInfo({
|
|
895
470
|
pinnedAddress,
|
|
896
471
|
pinnedGrammarVersion: PINNED_INTERPRETER_GRAMMAR_VERSION,
|
|
897
|
-
pinnedWasmHash:
|
|
472
|
+
pinnedWasmHash: PINNED_INTERPRETER_WASM_SHA256,
|
|
898
473
|
network,
|
|
899
474
|
...(deployedGrammarVersion !== undefined ? { deployedGrammarVersion } : {}),
|
|
900
475
|
})
|
|
901
476
|
return { ok: true, data: info }
|
|
902
477
|
} catch (e) {
|
|
903
|
-
return
|
|
904
|
-
ok: false,
|
|
905
|
-
error: caughtError('get_interpreter_info', 'RECORDING_FAILED', e),
|
|
906
|
-
}
|
|
478
|
+
return toolFailure('get_interpreter_info', e)
|
|
907
479
|
}
|
|
908
480
|
}
|
|
909
481
|
|
|
@@ -969,14 +541,8 @@ function enforceRpcPin(
|
|
|
969
541
|
network: Network
|
|
970
542
|
): ToolError | null {
|
|
971
543
|
if (!rpcUrl || rpcUrl === expectedRpc || allowUnpinned === true) return null
|
|
972
|
-
const code: ErrorCode =
|
|
973
|
-
toolName === 'install_policy'
|
|
974
|
-
? 'INSTALL_BUILD_FAILED'
|
|
975
|
-
: toolName === 'revoke_policy'
|
|
976
|
-
? 'REVOKE_BUILD_FAILED'
|
|
977
|
-
: 'RECORDING_FAILED'
|
|
978
544
|
return {
|
|
979
|
-
code,
|
|
545
|
+
code: TOOL_ERROR_CODE[toolName],
|
|
980
546
|
message: `${toolName}: rpcUrl must equal the pinned ${expectedRpc} (${network}); set allowUnpinnedRpcUrl: true to opt in to a custom endpoint`,
|
|
981
547
|
severity: 'error',
|
|
982
548
|
retryable: false,
|
|
@@ -1008,6 +574,12 @@ function validationError(
|
|
|
1008
574
|
* error in `details` for the agent to inspect. Exported as a test-only seam
|
|
1009
575
|
* so the suite in run/index.test.ts can drive the envelope path without
|
|
1010
576
|
* standing up a full recordTransaction pipeline. */
|
|
577
|
+
/** The fail-closed return every tool body uses in its `catch`. Taking only the
|
|
578
|
+
* tool name keeps the tool -> error-code mapping in TOOL_ERROR_CODE alone. */
|
|
579
|
+
function toolFailure(toolName: RunToolName, e: unknown): { ok: false; error: ToolError } {
|
|
580
|
+
return { ok: false, error: caughtError(toolName, TOOL_ERROR_CODE[toolName], e) }
|
|
581
|
+
}
|
|
582
|
+
|
|
1011
583
|
export function caughtError(toolName: RunToolName, code: ErrorCode, e: unknown): ToolError {
|
|
1012
584
|
return {
|
|
1013
585
|
code,
|