@crediolabs/policy-synth 0.3.1 → 0.5.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/dist/adapters/interpreter/adapter.d.ts +2 -2
- package/dist/adapters/interpreter/adapter.js +11 -3
- package/dist/errors.d.ts +6 -1
- package/dist/install/authority-overlap.d.ts +101 -0
- package/dist/install/authority-overlap.js +239 -0
- package/dist/install/build-add-context-rule.d.ts +1 -1
- package/dist/install/index.d.ts +1 -0
- package/dist/install/index.js +4 -0
- package/dist/install/read-account-rules.d.ts +79 -0
- package/dist/install/read-account-rules.js +241 -0
- package/dist/predicate/decode.js +22 -1
- package/dist/predicate/encode.js +52 -5
- package/dist/predicate/from-json.js +14 -1
- package/dist/record/index.d.ts +10 -0
- package/dist/record/index.js +32 -1
- package/dist/record/rpc.d.ts +4 -0
- package/dist/record/rpc.js +4 -1
- package/dist/registry/identify.d.ts +10 -1
- package/dist/registry/identify.js +4 -1
- package/dist/registry/on-chain-spec.d.ts +37 -0
- package/dist/registry/on-chain-spec.js +152 -0
- package/dist/review-card/builder.js +40 -0
- package/dist/review-card/cross-check.js +34 -0
- package/dist/review-card/render-leaf.d.ts +1 -1
- package/dist/review-card/render-leaf.js +7 -0
- package/dist/run/index.d.ts +25 -4
- package/dist/run/index.js +108 -4
- package/dist/run/schemas.d.ts +350 -6
- package/dist/run/schemas.js +95 -6
- package/dist/simulate/deny-cases.js +11 -0
- package/dist/simulate/evaluate.js +86 -5
- package/dist/synth/declare.d.ts +43 -0
- package/dist/synth/declare.js +127 -0
- package/dist/synth/index.d.ts +1 -0
- package/dist/synth/index.js +1 -0
- package/dist/synth/synthesize-from-recording.js +1 -1
- package/dist/types.d.ts +21 -1
- package/dist/types.js +1 -1
- package/dist-cjs/adapters/interpreter/adapter.d.ts +2 -2
- package/dist-cjs/adapters/interpreter/adapter.js +11 -3
- package/dist-cjs/errors.d.ts +6 -1
- package/dist-cjs/install/authority-overlap.d.ts +101 -0
- package/dist-cjs/install/authority-overlap.js +248 -0
- package/dist-cjs/install/build-add-context-rule.d.ts +1 -1
- package/dist-cjs/install/index.d.ts +1 -0
- package/dist-cjs/install/index.js +13 -2
- package/dist-cjs/install/read-account-rules.d.ts +79 -0
- package/dist-cjs/install/read-account-rules.js +252 -0
- package/dist-cjs/predicate/decode.js +22 -1
- package/dist-cjs/predicate/encode.js +52 -5
- package/dist-cjs/predicate/from-json.js +14 -1
- package/dist-cjs/record/index.d.ts +10 -0
- package/dist-cjs/record/index.js +31 -0
- package/dist-cjs/record/rpc.d.ts +4 -0
- package/dist-cjs/record/rpc.js +7 -3
- package/dist-cjs/registry/identify.d.ts +10 -1
- package/dist-cjs/registry/identify.js +4 -0
- package/dist-cjs/registry/on-chain-spec.d.ts +37 -0
- package/dist-cjs/registry/on-chain-spec.js +159 -0
- package/dist-cjs/review-card/builder.js +40 -0
- package/dist-cjs/review-card/cross-check.js +34 -0
- package/dist-cjs/review-card/render-leaf.d.ts +1 -1
- package/dist-cjs/review-card/render-leaf.js +7 -0
- package/dist-cjs/run/index.d.ts +25 -4
- package/dist-cjs/run/index.js +108 -2
- package/dist-cjs/run/schemas.d.ts +350 -6
- package/dist-cjs/run/schemas.js +96 -7
- package/dist-cjs/simulate/deny-cases.js +11 -0
- package/dist-cjs/simulate/evaluate.js +86 -5
- package/dist-cjs/synth/declare.d.ts +43 -0
- package/dist-cjs/synth/declare.js +130 -0
- package/dist-cjs/synth/index.d.ts +1 -0
- package/dist-cjs/synth/index.js +3 -1
- package/dist-cjs/synth/synthesize-from-recording.js +1 -1
- package/dist-cjs/types.d.ts +21 -1
- package/dist-cjs/types.js +1 -1
- package/package.json +1 -1
- package/src/adapters/interpreter/adapter.ts +13 -5
- package/src/errors.ts +5 -0
- package/src/install/authority-overlap.ts +323 -0
- package/src/install/index.ts +20 -0
- package/src/install/read-account-rules.ts +313 -0
- package/src/predicate/decode.ts +22 -1
- package/src/predicate/encode.ts +55 -5
- package/src/predicate/from-json.ts +14 -1
- package/src/record/index.ts +59 -2
- package/src/record/rpc.ts +4 -1
- package/src/registry/identify.ts +4 -1
- package/src/registry/on-chain-spec.ts +168 -0
- package/src/review-card/builder.ts +45 -2
- package/src/review-card/cross-check.ts +35 -1
- package/src/review-card/render-leaf.ts +8 -1
- package/src/run/index.ts +125 -2
- package/src/run/schemas.ts +100 -6
- package/src/simulate/deny-cases.ts +12 -1
- package/src/simulate/evaluate.ts +101 -9
- package/src/synth/declare.ts +206 -0
- package/src/synth/index.ts +5 -0
- package/src/synth/synthesize-from-recording.ts +1 -1
- package/src/types.ts +16 -1
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// src/registry/on-chain-spec.ts - read a contract's own interface off chain.
|
|
2
|
+
//
|
|
3
|
+
// The compiled-in registry covers the protocols we pinned by hand. Everything
|
|
4
|
+
// else was reported as `no-abi`, which read as "this contract does not
|
|
5
|
+
// describe itself" when it almost always does: a Soroban contract embeds a
|
|
6
|
+
// typed spec in its wasm, and the network will hand it over. The recorder was
|
|
7
|
+
// refusing calls whose interface was one RPC round-trip away.
|
|
8
|
+
//
|
|
9
|
+
// What this buys is NARROW and deliberately so. A fetched spec says what the
|
|
10
|
+
// contract's arguments ARE, not what they MEAN. So a call verified against it
|
|
11
|
+
// is recognised - the decode is trustworthy, the confidence gate stops
|
|
12
|
+
// refusing it - but the arguments carry the contract's own parameter NAMES
|
|
13
|
+
// rather than a curated meaning, and nothing here infers which argument is a
|
|
14
|
+
// spend or a recipient. A pinned protocol still outranks a fetched spec for
|
|
15
|
+
// exactly that reason, and `identifyProtocol` is consulted first.
|
|
16
|
+
import { contract as sdkContract } from '@stellar/stellar-sdk';
|
|
17
|
+
import { argsMatchAbi } from "./identify.js";
|
|
18
|
+
/** Map an XDR spec type to the ScVal subset vocabulary the matcher uses.
|
|
19
|
+
*
|
|
20
|
+
* Returns null for a type the recorder's ScVal subset cannot represent. That
|
|
21
|
+
* is deliberate: an argument we cannot type is an argument we cannot check,
|
|
22
|
+
* and claiming a match on it would be the fail-OPEN direction. A function
|
|
23
|
+
* with any such argument is dropped from the derived ABI, so a call to it
|
|
24
|
+
* stays unrecognised rather than being waved through. */
|
|
25
|
+
export function abiTypeFromSpecType(specTypeName) {
|
|
26
|
+
switch (specTypeName) {
|
|
27
|
+
case 'scSpecTypeAddress':
|
|
28
|
+
return 'address';
|
|
29
|
+
case 'scSpecTypeI128':
|
|
30
|
+
return 'i128';
|
|
31
|
+
case 'scSpecTypeU64':
|
|
32
|
+
case 'scSpecTypeI64':
|
|
33
|
+
return 'u64';
|
|
34
|
+
case 'scSpecTypeU32':
|
|
35
|
+
case 'scSpecTypeI32':
|
|
36
|
+
return 'u32';
|
|
37
|
+
case 'scSpecTypeSymbol':
|
|
38
|
+
return 'symbol';
|
|
39
|
+
case 'scSpecTypeVec':
|
|
40
|
+
return 'vec';
|
|
41
|
+
case 'scSpecTypeBytes':
|
|
42
|
+
case 'scSpecTypeBytesN':
|
|
43
|
+
return 'bytes';
|
|
44
|
+
case 'scSpecTypeMap':
|
|
45
|
+
return 'map';
|
|
46
|
+
default:
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Build a `SpecFetcher` over the SDK's contract client. */
|
|
51
|
+
export function specFetcherFromRpc(rpcUrl, networkPassphrase) {
|
|
52
|
+
return {
|
|
53
|
+
async contractSpec(contractId) {
|
|
54
|
+
try {
|
|
55
|
+
const client = await sdkContract.Client.from({ contractId, networkPassphrase, rpcUrl });
|
|
56
|
+
return client.spec ?? null;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// A contract with no spec, an unreachable RPC and a bad address all
|
|
60
|
+
// land here and all mean the same thing to the caller: no interface
|
|
61
|
+
// was obtained, so the contract stays unrecognised. Fail closed.
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** Convert a fetched spec into the ABI shape `identifyProtocol` already
|
|
68
|
+
* matches against. Functions with an argument outside the ScVal subset are
|
|
69
|
+
* OMITTED rather than partially typed. */
|
|
70
|
+
export function abiFromSpec(spec) {
|
|
71
|
+
const abi = {};
|
|
72
|
+
for (const fn of spec.funcs()) {
|
|
73
|
+
const name = fn.name().toString();
|
|
74
|
+
// The constructor is not callable after deployment, so a recorded
|
|
75
|
+
// invocation can never be one. Keeping it would only widen the surface.
|
|
76
|
+
if (name === '__constructor')
|
|
77
|
+
continue;
|
|
78
|
+
const args = [];
|
|
79
|
+
let usable = true;
|
|
80
|
+
for (const input of fn.inputs()) {
|
|
81
|
+
const type = abiTypeFromSpecType(input.type().switch().name);
|
|
82
|
+
if (type === null) {
|
|
83
|
+
usable = false;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
const argName = input.name().toString();
|
|
87
|
+
args.push({
|
|
88
|
+
name: argName,
|
|
89
|
+
type,
|
|
90
|
+
// The contract's own parameter name is the honest description. It is
|
|
91
|
+
// NOT a curated meaning - nothing here knows whether `value` is a
|
|
92
|
+
// spend - so downstream must not treat it as one.
|
|
93
|
+
meaning: `${argName} (from the contract's on-chain interface)`,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (usable)
|
|
97
|
+
abi[name] = { args };
|
|
98
|
+
}
|
|
99
|
+
return abi;
|
|
100
|
+
}
|
|
101
|
+
/** Fetch and convert in one step. Returns null when no usable interface was
|
|
102
|
+
* obtained, which the caller must treat as "unrecognised". */
|
|
103
|
+
export async function fetchContractAbi(contractId, fetcher) {
|
|
104
|
+
const spec = await fetcher.contractSpec(contractId);
|
|
105
|
+
if (spec === null)
|
|
106
|
+
return null;
|
|
107
|
+
const abi = abiFromSpec(spec);
|
|
108
|
+
return Object.keys(abi).length === 0 ? null : abi;
|
|
109
|
+
}
|
|
110
|
+
/** Every contract invoked anywhere in the tree, with the calls made on it. */
|
|
111
|
+
function callsByContract(invocations) {
|
|
112
|
+
const out = new Map();
|
|
113
|
+
const walk = (inv) => {
|
|
114
|
+
const list = out.get(inv.contract) ?? [];
|
|
115
|
+
list.push({ fn: inv.fn, args: inv.args });
|
|
116
|
+
out.set(inv.contract, list);
|
|
117
|
+
for (const sub of inv.subInvocations)
|
|
118
|
+
walk(sub);
|
|
119
|
+
};
|
|
120
|
+
for (const inv of invocations)
|
|
121
|
+
walk(inv);
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
/** Contracts whose EVERY recorded call matches their own published interface.
|
|
125
|
+
*
|
|
126
|
+
* All-or-nothing per contract, deliberately. A contract where one call
|
|
127
|
+
* verifies and another does not is a contract we do not understand, and
|
|
128
|
+
* marking it recognised would raise confidence on the strength of the call
|
|
129
|
+
* we happened to check. The unverified call is the one that matters.
|
|
130
|
+
*
|
|
131
|
+
* A contract with no fetchable spec, or a call naming a function absent from
|
|
132
|
+
* it, simply stays unknown - this only ever ADDS recognition, so a failure
|
|
133
|
+
* here degrades to today's behaviour rather than to a wrong answer. */
|
|
134
|
+
export async function resolveContractsByOnChainSpec(invocations, candidates, fetcher) {
|
|
135
|
+
const resolved = new Set();
|
|
136
|
+
const calls = callsByContract(invocations);
|
|
137
|
+
for (const contract of new Set(candidates)) {
|
|
138
|
+
const made = calls.get(contract);
|
|
139
|
+
if (!made || made.length === 0)
|
|
140
|
+
continue;
|
|
141
|
+
const abi = await fetchContractAbi(contract, fetcher);
|
|
142
|
+
if (abi === null)
|
|
143
|
+
continue;
|
|
144
|
+
const everyCallVerifies = made.every((c) => {
|
|
145
|
+
const entry = abi[c.fn];
|
|
146
|
+
return entry !== undefined && argsMatchAbi(entry.args, c.args);
|
|
147
|
+
});
|
|
148
|
+
if (everyCallVerifies)
|
|
149
|
+
resolved.add(contract);
|
|
150
|
+
}
|
|
151
|
+
return resolved;
|
|
152
|
+
}
|
|
@@ -80,15 +80,31 @@ function walkPredicate(node, visit) {
|
|
|
80
80
|
for (const child of node.children)
|
|
81
81
|
walkPredicate(child, visit);
|
|
82
82
|
return;
|
|
83
|
+
// NOT descended into. Every line the card emits reads as a requirement,
|
|
84
|
+
// and `and` is what makes that true. Listing an `or`'s branches as
|
|
85
|
+
// separate lines would state the opposite of what the policy means, so
|
|
86
|
+
// the whole disjunction is rendered as ONE line instead.
|
|
87
|
+
case 'or':
|
|
88
|
+
visit(node);
|
|
89
|
+
return;
|
|
83
90
|
case 'in':
|
|
84
91
|
visit(node);
|
|
85
92
|
return;
|
|
86
93
|
case 'eq':
|
|
94
|
+
case 'lt':
|
|
87
95
|
case 'lte':
|
|
96
|
+
case 'gt':
|
|
97
|
+
case 'gte':
|
|
88
98
|
visit(node);
|
|
89
99
|
return;
|
|
90
100
|
}
|
|
91
101
|
}
|
|
102
|
+
/** Argument index of a `call_arg` leaf, for the scaled-comparison line. Any
|
|
103
|
+
* other leaf renders as its kind so the line stays readable rather than
|
|
104
|
+
* claiming an index that does not exist. */
|
|
105
|
+
function leftArgLabel(leaf) {
|
|
106
|
+
return leaf.kind === 'call_arg' ? String(leaf.index) : `<${leaf.kind}>`;
|
|
107
|
+
}
|
|
92
108
|
/** Render ONE constraint sentence for ONE interpreter predicate node. The
|
|
93
109
|
* shape of the output is pinned by Task 7b so the test suite can assert
|
|
94
110
|
* byte-for-byte equality. Returns `null` when the node is a structural
|
|
@@ -97,14 +113,38 @@ function renderConstraint(node) {
|
|
|
97
113
|
switch (node.op) {
|
|
98
114
|
case 'and':
|
|
99
115
|
return null;
|
|
116
|
+
case 'or': {
|
|
117
|
+
// One line for the whole disjunction. If any branch is a shape the
|
|
118
|
+
// card cannot render, the entire line is withheld rather than shown
|
|
119
|
+
// with a branch missing - a disjunction with a branch dropped reads
|
|
120
|
+
// as STRICTER than it is, which is the dangerous direction.
|
|
121
|
+
const parts = node.children.map(renderConstraint);
|
|
122
|
+
if (parts.some((p) => p === null))
|
|
123
|
+
return null;
|
|
124
|
+
return `Either: ${parts.join(' OR ')}`;
|
|
125
|
+
}
|
|
100
126
|
case 'eq':
|
|
127
|
+
case 'lt':
|
|
101
128
|
case 'lte':
|
|
129
|
+
case 'gt':
|
|
130
|
+
case 'gte':
|
|
102
131
|
return renderComparison(node);
|
|
103
132
|
case 'in':
|
|
104
133
|
return renderMembership(node);
|
|
105
134
|
}
|
|
106
135
|
}
|
|
107
136
|
function renderComparison(node) {
|
|
137
|
+
// The slippage floor: OP(call_arg[out], call_arg_scaled(in, num, den)).
|
|
138
|
+
// Rendered explicitly because the human approving the signature has to see
|
|
139
|
+
// that the bound is a RATIO of another argument, not a fixed amount.
|
|
140
|
+
if (node.right.kind === 'call_arg_scaled') {
|
|
141
|
+
const s = node.right;
|
|
142
|
+
return `arg[${leftArgLabel(node.left)}] ${comparisonOpText(node.op)} arg[${s.index}] * ${s.num}/${s.den}`;
|
|
143
|
+
}
|
|
144
|
+
if (node.left.kind === 'call_arg_scaled') {
|
|
145
|
+
const s = node.left;
|
|
146
|
+
return `arg[${s.index}] * ${s.num}/${s.den} ${comparisonOpText(node.op)} arg[${leftArgLabel(node.right)}]`;
|
|
147
|
+
}
|
|
108
148
|
const left = node.left;
|
|
109
149
|
const right = node.right;
|
|
110
150
|
// eq(call_contract, literal_address) -> Contract must be <addr>
|
|
@@ -39,8 +39,30 @@ function collect(node, out) {
|
|
|
39
39
|
for (const child of node.children)
|
|
40
40
|
collect(child, out);
|
|
41
41
|
return;
|
|
42
|
+
// ONE line for the whole disjunction, mirroring the builder. Emitting a
|
|
43
|
+
// line per branch would claim every branch is required, which is the
|
|
44
|
+
// opposite of what `or` means. If any branch renders to nothing the whole
|
|
45
|
+
// line is withheld, again mirroring the builder - a disjunction missing a
|
|
46
|
+
// branch reads STRICTER than it is.
|
|
47
|
+
case 'or': {
|
|
48
|
+
const parts = [];
|
|
49
|
+
for (const child of node.children) {
|
|
50
|
+
const childOut = [];
|
|
51
|
+
collect(child, childOut);
|
|
52
|
+
if (childOut.length !== 1)
|
|
53
|
+
return;
|
|
54
|
+
parts.push(childOut[0]);
|
|
55
|
+
}
|
|
56
|
+
if (parts.length === 0)
|
|
57
|
+
return;
|
|
58
|
+
out.push(`Either: ${parts.join(' OR ')}`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
42
61
|
case 'eq':
|
|
62
|
+
case 'lt':
|
|
43
63
|
case 'lte':
|
|
64
|
+
case 'gt':
|
|
65
|
+
case 'gte':
|
|
44
66
|
pushComparison(node.left, node.right, node.op, out);
|
|
45
67
|
return;
|
|
46
68
|
case 'in':
|
|
@@ -49,6 +71,18 @@ function collect(node, out) {
|
|
|
49
71
|
}
|
|
50
72
|
}
|
|
51
73
|
function pushComparison(left, right, op, out) {
|
|
74
|
+
// Slippage floor, mirroring the builder. The human has to see that the
|
|
75
|
+
// bound is a RATIO of another argument, not a fixed amount.
|
|
76
|
+
if (right.kind === 'call_arg_scaled') {
|
|
77
|
+
const label = left.kind === 'call_arg' ? String(left.index) : `<${left.kind}>`;
|
|
78
|
+
out.push(`arg[${label}] ${comparisonOpText(op)} arg[${right.index}] * ${right.num}/${right.den}`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (left.kind === 'call_arg_scaled') {
|
|
82
|
+
const label = right.kind === 'call_arg' ? String(right.index) : `<${right.kind}>`;
|
|
83
|
+
out.push(`arg[${left.index}] * ${left.num}/${left.den} ${comparisonOpText(op)} arg[${label}]`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
52
86
|
if (left.kind === 'call_contract' && op === 'eq' && right.kind === 'literal_address') {
|
|
53
87
|
out.push(`Contract must be ${right.value}`);
|
|
54
88
|
return;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { PredicateLeaf } from '../types.ts';
|
|
2
2
|
export declare function renderVecElement(leaf: PredicateLeaf): string;
|
|
3
3
|
export declare function renderHaystackElement(leaf: PredicateLeaf): string;
|
|
4
|
-
export declare function comparisonOpText(op: 'eq' | 'lte'): string;
|
|
4
|
+
export declare function comparisonOpText(op: 'eq' | 'lt' | 'lte' | 'gt' | 'gte'): string;
|
|
@@ -20,6 +20,7 @@ export function renderVecElement(leaf) {
|
|
|
20
20
|
case 'call_arg':
|
|
21
21
|
case 'call_arg_len':
|
|
22
22
|
case 'call_arg_field':
|
|
23
|
+
case 'call_arg_scaled':
|
|
23
24
|
return `<${leaf.kind}>`;
|
|
24
25
|
}
|
|
25
26
|
}
|
|
@@ -39,8 +40,14 @@ export function renderHaystackElement(leaf) {
|
|
|
39
40
|
}
|
|
40
41
|
export function comparisonOpText(op) {
|
|
41
42
|
switch (op) {
|
|
43
|
+
case 'lt':
|
|
44
|
+
return '<';
|
|
42
45
|
case 'lte':
|
|
43
46
|
return '<=';
|
|
47
|
+
case 'gt':
|
|
48
|
+
return '>';
|
|
49
|
+
case 'gte':
|
|
50
|
+
return '>=';
|
|
44
51
|
case 'eq':
|
|
45
52
|
return '==';
|
|
46
53
|
}
|
package/dist/run/index.d.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { type ErrorCode, type PredicateNode, type ProposedPolicy, type RecordedTransaction, type ToolError, type ToolResponse } from '../index.ts';
|
|
2
|
+
import { type AuthorityOverlap } from '../install/authority-overlap.ts';
|
|
2
3
|
import { type BuildInstallPolicyResult, type BuildRevokePolicyResult } from '../install/build-install-policy.ts';
|
|
3
4
|
import { getInterpreterInfo } from '../install/get-interpreter-info.ts';
|
|
4
5
|
import { type RecordTransactionInput, type SimulatePolicyInput, type SynthesizePolicyInput, type VerifyPolicyInput } from './schemas.ts';
|
|
5
|
-
export type { GetInterpreterInfoInput, InstallPolicyInput, RecordTransactionInput, RevokePolicyInput, SimulatePolicyInput, SynthesizePolicyInput, VerifyPolicyInput, } from './schemas.ts';
|
|
6
|
-
export { ComposeUserResponsesSchema, GetInterpreterInfoInputSchema, InstallPolicyInputSchema, InterpreterOptionsSchema, MAINNET_RPC_URL, NetworkSchema, PINNED_INTERPRETER_ADDRESS_BY_NETWORK, PINNED_INTERPRETER_GRAMMAR_VERSION, PINNED_INTERPRETER_MAINNET_ADDRESS, PINNED_INTERPRETER_TESTNET_ADDRESS, PINNED_INTERPRETER_WASM_SHA256, PredicateLeafSchema, PredicateNodeSchema, RecordedTransactionSchema, RecordTransactionInputSchema, RevokePolicyInputSchema, RPC_URL_BY_NETWORK, SynthesizePolicyInputSchema, TESTNET_RPC_URL, ToolErrorSchema, } from './schemas.ts';
|
|
6
|
+
export type { DeclarePolicyInput, GetInterpreterInfoInput, InstallPolicyInput, RecordTransactionInput, RevokePolicyInput, SimulatePolicyInput, SynthesizePolicyInput, VerifyPolicyInput, } from './schemas.ts';
|
|
7
|
+
export { ComposeUserResponsesSchema, DeclarePolicyInputSchema, GetInterpreterInfoInputSchema, InstallPolicyInputSchema, InterpreterOptionsSchema, MAINNET_RPC_URL, NetworkSchema, PINNED_INTERPRETER_ADDRESS_BY_NETWORK, PINNED_INTERPRETER_GRAMMAR_VERSION, PINNED_INTERPRETER_MAINNET_ADDRESS, PINNED_INTERPRETER_TESTNET_ADDRESS, PINNED_INTERPRETER_WASM_SHA256, PredicateLeafSchema, PredicateNodeSchema, RecordedTransactionSchema, RecordTransactionInputSchema, RevokePolicyInputSchema, RPC_URL_BY_NETWORK, SynthesizePolicyInputSchema, TESTNET_RPC_URL, ToolErrorSchema, } from './schemas.ts';
|
|
7
8
|
export type RunRecordTransactionInput = RecordTransactionInput;
|
|
8
9
|
export type RunSynthesizePolicyInput = SynthesizePolicyInput;
|
|
9
10
|
export type RunSimulatePolicyInput = SimulatePolicyInput;
|
|
10
11
|
export type RunVerifyPolicyInput = VerifyPolicyInput;
|
|
11
|
-
type RunToolName = 'record_transaction' | 'synthesize_policy' | 'simulate_policy' | 'verify_policy' | 'install_policy' | 'revoke_policy' | 'get_interpreter_info';
|
|
12
|
+
type RunToolName = 'record_transaction' | 'synthesize_policy' | 'declare_policy' | 'simulate_policy' | 'verify_policy' | 'install_policy' | 'revoke_policy' | 'get_interpreter_info';
|
|
12
13
|
/** `record_transaction` body - wraps `recordTransaction`. The tool input
|
|
13
14
|
* matches the core RecordInput minus the injected `fetcher` (the transport
|
|
14
15
|
* layer does not own the RPC). Returns the core ToolResponse unchanged.
|
|
@@ -37,7 +38,9 @@ export declare function runSynthesizePolicy(raw: unknown): Promise<ToolResponse<
|
|
|
37
38
|
predicateTree: PredicateNode | null;
|
|
38
39
|
};
|
|
39
40
|
}>;
|
|
40
|
-
export declare function runInstallPolicy(raw: unknown): Promise<ToolResponse<BuildInstallPolicyResult
|
|
41
|
+
export declare function runInstallPolicy(raw: unknown): Promise<ToolResponse<BuildInstallPolicyResult & {
|
|
42
|
+
authorityScan: AuthorityOverlap[] | null;
|
|
43
|
+
}>>;
|
|
41
44
|
/** `revoke_policy` body - thin wrapper over `buildRevokePolicyXdr`.
|
|
42
45
|
* Emits an unsigned XDR for `account.remove_context_rule(ruleId)`; the
|
|
43
46
|
* smart account itself handles uninstalling each attached policy. Auth
|
|
@@ -63,6 +66,24 @@ export declare function runSimulatePolicy(raw: unknown): ToolResponse<{
|
|
|
63
66
|
argCount: number;
|
|
64
67
|
};
|
|
65
68
|
}>;
|
|
69
|
+
/** `declare_policy` body - the DECLARATIVE front-end.
|
|
70
|
+
*
|
|
71
|
+
* `synthesize_policy` infers a predicate from a transaction that happened;
|
|
72
|
+
* this takes the constraint stated outright. No RPC, no decoding and no
|
|
73
|
+
* parseConfidence, so nothing here can be refused for a contract the registry
|
|
74
|
+
* does not recognise - which is most of the point of having it.
|
|
75
|
+
*
|
|
76
|
+
* The returned `warnings` are load-bearing, not decoration. An argument index
|
|
77
|
+
* the caller did not supply is DEFAULTED to the SEP-41 position, and a bound
|
|
78
|
+
* on the wrong argument constrains something the caller did not mean without
|
|
79
|
+
* ever announcing itself, so a caller that ignores warnings can install a
|
|
80
|
+
* predicate that reads correctly and binds nothing. */
|
|
81
|
+
export declare function runDeclarePolicy(raw: unknown): ToolResponse<{
|
|
82
|
+
predicate: PredicateNode;
|
|
83
|
+
encodedPredicate: string;
|
|
84
|
+
predicateHash: string;
|
|
85
|
+
warnings: string[];
|
|
86
|
+
}>;
|
|
66
87
|
/** `verify_policy` body - the permit case plus a generated deny case per
|
|
67
88
|
* dimension.
|
|
68
89
|
*
|
package/dist/run/index.js
CHANGED
|
@@ -18,22 +18,26 @@
|
|
|
18
18
|
// drive the CLI (which calls into the same core directly without MCP).
|
|
19
19
|
import { createHash } from 'node:crypto';
|
|
20
20
|
import { rpc } from '@stellar/stellar-sdk';
|
|
21
|
-
import { recordTransaction, synthesizeFromRecording, } from "../index.js";
|
|
21
|
+
import { declarePredicate, encodePredicate, recordTransaction, synthesizeFromRecording, } from "../index.js";
|
|
22
|
+
import { findAuthorityOverlaps, } from "../install/authority-overlap.js";
|
|
22
23
|
import { buildInstallPolicyXdr, buildRevokePolicyXdr, rpcClientFromServer, } from "../install/build-install-policy.js";
|
|
23
24
|
import { getInterpreterInfo } from "../install/get-interpreter-info.js";
|
|
25
|
+
import { accountRuleReaderFromServer, collectObservedRules } from "../install/read-account-rules.js";
|
|
26
|
+
import { decodePredicate } from "../predicate/decode.js";
|
|
24
27
|
import { evaluate, generateCases } from "../simulate/index.js";
|
|
25
|
-
import { GetInterpreterInfoInputSchema, InstallPolicyInputSchema, NETWORK_PASSPHRASES, PINNED_INTERPRETER_ADDRESS_BY_NETWORK, PINNED_INTERPRETER_GRAMMAR_VERSION, PINNED_INTERPRETER_WASM_SHA256, RecordTransactionInputSchema, RevokePolicyInputSchema, RPC_URL_BY_NETWORK, SimulatePolicyInputSchema, SynthesizePolicyInputSchema, VerifyPolicyInputSchema, } from "./schemas.js";
|
|
28
|
+
import { DeclarePolicyInputSchema, GetInterpreterInfoInputSchema, InstallPolicyInputSchema, NETWORK_PASSPHRASES, PINNED_INTERPRETER_ADDRESS_BY_NETWORK, PINNED_INTERPRETER_GRAMMAR_VERSION, PINNED_INTERPRETER_WASM_SHA256, RecordTransactionInputSchema, RevokePolicyInputSchema, RPC_URL_BY_NETWORK, SimulatePolicyInputSchema, SynthesizePolicyInputSchema, VerifyPolicyInputSchema, } from "./schemas.js";
|
|
26
29
|
// Re-export the underlying Zod schemas so the MCP package (and any other
|
|
27
30
|
// downstream consumer) can import the canonical input shapes from the same
|
|
28
31
|
// module that owns the tool-body glue. The strict schemas are the source of
|
|
29
32
|
// truth - MCP tool shapes are derived from them.
|
|
30
|
-
export { ComposeUserResponsesSchema, GetInterpreterInfoInputSchema, InstallPolicyInputSchema, InterpreterOptionsSchema, MAINNET_RPC_URL, NetworkSchema, PINNED_INTERPRETER_ADDRESS_BY_NETWORK, PINNED_INTERPRETER_GRAMMAR_VERSION, PINNED_INTERPRETER_MAINNET_ADDRESS, PINNED_INTERPRETER_TESTNET_ADDRESS, PINNED_INTERPRETER_WASM_SHA256, PredicateLeafSchema, PredicateNodeSchema, RecordedTransactionSchema, RecordTransactionInputSchema, RevokePolicyInputSchema, RPC_URL_BY_NETWORK, SynthesizePolicyInputSchema, TESTNET_RPC_URL, ToolErrorSchema, } from "./schemas.js";
|
|
33
|
+
export { ComposeUserResponsesSchema, DeclarePolicyInputSchema, GetInterpreterInfoInputSchema, InstallPolicyInputSchema, InterpreterOptionsSchema, MAINNET_RPC_URL, NetworkSchema, PINNED_INTERPRETER_ADDRESS_BY_NETWORK, PINNED_INTERPRETER_GRAMMAR_VERSION, PINNED_INTERPRETER_MAINNET_ADDRESS, PINNED_INTERPRETER_TESTNET_ADDRESS, PINNED_INTERPRETER_WASM_SHA256, PredicateLeafSchema, PredicateNodeSchema, RecordedTransactionSchema, RecordTransactionInputSchema, RevokePolicyInputSchema, RPC_URL_BY_NETWORK, SynthesizePolicyInputSchema, TESTNET_RPC_URL, ToolErrorSchema, } from "./schemas.js";
|
|
31
34
|
/** Map every tool name to its canonical domain error code. Replaces a 7-way
|
|
32
35
|
* if/else so adding a tool adds one line here rather than a new branch in
|
|
33
36
|
* each envelope call. */
|
|
34
37
|
const TOOL_ERROR_CODE = {
|
|
35
38
|
record_transaction: 'RECORDING_FAILED',
|
|
36
39
|
synthesize_policy: 'SYNTHESIS_ERROR',
|
|
40
|
+
declare_policy: 'SYNTHESIS_ERROR',
|
|
37
41
|
simulate_policy: 'SIMULATION_ERROR',
|
|
38
42
|
verify_policy: 'VERIFICATION_FAILED',
|
|
39
43
|
install_policy: 'INSTALL_BUILD_FAILED',
|
|
@@ -148,7 +152,32 @@ export async function runInstallPolicy(raw) {
|
|
|
148
152
|
rpc: rpcClient,
|
|
149
153
|
...(input.baseFee !== undefined ? { baseFee: input.baseFee } : {}),
|
|
150
154
|
});
|
|
151
|
-
|
|
155
|
+
// Cross-rule scan. The caller may supply `existingRules` (useful offline,
|
|
156
|
+
// and for testing); otherwise the account is READ, so the answer describes
|
|
157
|
+
// what is actually installed rather than what the caller happened to
|
|
158
|
+
// mention.
|
|
159
|
+
//
|
|
160
|
+
// `null` means NOT CHECKED and is returned whenever the scan cannot be
|
|
161
|
+
// trusted to be complete - the read failed, or it stopped before
|
|
162
|
+
// accounting for every live rule. An empty list would say "checked,
|
|
163
|
+
// nothing found", and a partial scan that reported `[]` would be claiming
|
|
164
|
+
// a safety it never established.
|
|
165
|
+
const observed = await resolveExistingRules(input, network, expectedInterpreter);
|
|
166
|
+
const authorityScan = observed === null
|
|
167
|
+
? null
|
|
168
|
+
: findAuthorityOverlaps({
|
|
169
|
+
intended: {
|
|
170
|
+
// `add_context_rule` gets its id FROM the account, so there is
|
|
171
|
+
// no existing rule this install replaces. A sentinel no real id
|
|
172
|
+
// can equal keeps every observed rule in scope.
|
|
173
|
+
ruleId: -1,
|
|
174
|
+
contextType: input.rule.contextRuleType,
|
|
175
|
+
signers: input.rule.signers,
|
|
176
|
+
predicate: decodePredicate(encodedPredicate),
|
|
177
|
+
},
|
|
178
|
+
existing: observed,
|
|
179
|
+
});
|
|
180
|
+
return { ok: true, data: { ...result, authorityScan } };
|
|
152
181
|
}
|
|
153
182
|
catch (e) {
|
|
154
183
|
return toolFailure('install_policy', e);
|
|
@@ -267,6 +296,45 @@ export function runSimulatePolicy(raw) {
|
|
|
267
296
|
return toolFailure('simulate_policy', e);
|
|
268
297
|
}
|
|
269
298
|
}
|
|
299
|
+
/** `declare_policy` body - the DECLARATIVE front-end.
|
|
300
|
+
*
|
|
301
|
+
* `synthesize_policy` infers a predicate from a transaction that happened;
|
|
302
|
+
* this takes the constraint stated outright. No RPC, no decoding and no
|
|
303
|
+
* parseConfidence, so nothing here can be refused for a contract the registry
|
|
304
|
+
* does not recognise - which is most of the point of having it.
|
|
305
|
+
*
|
|
306
|
+
* The returned `warnings` are load-bearing, not decoration. An argument index
|
|
307
|
+
* the caller did not supply is DEFAULTED to the SEP-41 position, and a bound
|
|
308
|
+
* on the wrong argument constrains something the caller did not mean without
|
|
309
|
+
* ever announcing itself, so a caller that ignores warnings can install a
|
|
310
|
+
* predicate that reads correctly and binds nothing. */
|
|
311
|
+
export function runDeclarePolicy(raw) {
|
|
312
|
+
const parsed = DeclarePolicyInputSchema.safeParse(raw);
|
|
313
|
+
if (!parsed.success) {
|
|
314
|
+
return { ok: false, error: validationError('declare_policy', parsed.error.issues) };
|
|
315
|
+
}
|
|
316
|
+
try {
|
|
317
|
+
// Rebuilt field-by-field rather than passed through: the schema's
|
|
318
|
+
// optionals are `T | undefined` and `PolicyDeclaration`'s are absent-or-T,
|
|
319
|
+
// which `exactOptionalPropertyTypes` treats as different.
|
|
320
|
+
const d = parsed.data;
|
|
321
|
+
const { predicate, warnings } = declarePredicate({
|
|
322
|
+
fn: d.fn,
|
|
323
|
+
...(d.contract !== undefined ? { contract: d.contract } : {}),
|
|
324
|
+
...(d.maxAmount !== undefined ? { maxAmount: d.maxAmount } : {}),
|
|
325
|
+
...(d.amountArgIndex !== undefined ? { amountArgIndex: d.amountArgIndex } : {}),
|
|
326
|
+
...(d.recipients !== undefined ? { recipients: d.recipients } : {}),
|
|
327
|
+
...(d.recipientArgIndex !== undefined ? { recipientArgIndex: d.recipientArgIndex } : {}),
|
|
328
|
+
...(d.allowZeroCap !== undefined ? { allowZeroCap: d.allowZeroCap } : {}),
|
|
329
|
+
...(d.minOutputRatio !== undefined ? { minOutputRatio: d.minOutputRatio } : {}),
|
|
330
|
+
});
|
|
331
|
+
const { encodedPredicate, predicateHash } = encodePredicate(predicate);
|
|
332
|
+
return { ok: true, data: { predicate, encodedPredicate, predicateHash, warnings } };
|
|
333
|
+
}
|
|
334
|
+
catch (e) {
|
|
335
|
+
return toolFailure('declare_policy', e);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
270
338
|
/** `verify_policy` body - the permit case plus a generated deny case per
|
|
271
339
|
* dimension.
|
|
272
340
|
*
|
|
@@ -354,6 +422,42 @@ export async function runGetInterpreterInfo(raw) {
|
|
|
354
422
|
* network, falling back to the pinned RPC for the network. The caller
|
|
355
423
|
* has already been gated against the pinned URL elsewhere, so the
|
|
356
424
|
* fallback here only ever picks from a finite, audited pair. */
|
|
425
|
+
/** The account's other context rules, or `null` when they could not be
|
|
426
|
+
* established completely.
|
|
427
|
+
*
|
|
428
|
+
* Caller-supplied `existingRules` win: they let the scan run offline, and a
|
|
429
|
+
* caller who passes them has said what to compare against. Otherwise the
|
|
430
|
+
* account is read over RPC.
|
|
431
|
+
*
|
|
432
|
+
* Every failure path returns `null` rather than a short list. A read that
|
|
433
|
+
* threw, or one that stopped before accounting for every live rule, has not
|
|
434
|
+
* ruled anything out - and reporting `[]` there would turn "we could not
|
|
435
|
+
* check" into "there is nothing to worry about". */
|
|
436
|
+
async function resolveExistingRules(input, network, interpreterAddress) {
|
|
437
|
+
if (input.existingRules !== undefined) {
|
|
438
|
+
// The schema types `predicate` loosely (it is the shared
|
|
439
|
+
// PredicateNodeSchema); the shape is already validated.
|
|
440
|
+
return input.existingRules;
|
|
441
|
+
}
|
|
442
|
+
try {
|
|
443
|
+
const url = input.rpcUrl ?? RPC_URL_BY_NETWORK[network];
|
|
444
|
+
const server = new rpc.Server(url, { allowHttp: false });
|
|
445
|
+
const collected = await collectObservedRules({
|
|
446
|
+
reader: accountRuleReaderFromServer(server, NETWORK_PASSPHRASES[network]),
|
|
447
|
+
smartAccount: input.smartAccount,
|
|
448
|
+
interpreterAddress,
|
|
449
|
+
});
|
|
450
|
+
if (collected.incomplete)
|
|
451
|
+
return null;
|
|
452
|
+
return collected.rules;
|
|
453
|
+
}
|
|
454
|
+
catch {
|
|
455
|
+
// The install itself is unaffected: the scan is advisory, so a failed
|
|
456
|
+
// read must not block a policy the user asked for. It just cannot be
|
|
457
|
+
// reported as a clean scan.
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
357
461
|
function buildRpcClientFromInput(urlOverride, network) {
|
|
358
462
|
const url = urlOverride ?? RPC_URL_BY_NETWORK[network];
|
|
359
463
|
const passphrase = NETWORK_PASSPHRASES[network];
|