@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.
Files changed (100) hide show
  1. package/dist/adapters/interpreter/adapter.d.ts +2 -2
  2. package/dist/adapters/interpreter/adapter.js +11 -3
  3. package/dist/errors.d.ts +6 -1
  4. package/dist/install/authority-overlap.d.ts +101 -0
  5. package/dist/install/authority-overlap.js +239 -0
  6. package/dist/install/build-add-context-rule.d.ts +1 -1
  7. package/dist/install/index.d.ts +1 -0
  8. package/dist/install/index.js +4 -0
  9. package/dist/install/read-account-rules.d.ts +79 -0
  10. package/dist/install/read-account-rules.js +241 -0
  11. package/dist/predicate/decode.js +22 -1
  12. package/dist/predicate/encode.js +52 -5
  13. package/dist/predicate/from-json.js +14 -1
  14. package/dist/record/index.d.ts +10 -0
  15. package/dist/record/index.js +32 -1
  16. package/dist/record/rpc.d.ts +4 -0
  17. package/dist/record/rpc.js +4 -1
  18. package/dist/registry/identify.d.ts +10 -1
  19. package/dist/registry/identify.js +4 -1
  20. package/dist/registry/on-chain-spec.d.ts +37 -0
  21. package/dist/registry/on-chain-spec.js +152 -0
  22. package/dist/review-card/builder.js +40 -0
  23. package/dist/review-card/cross-check.js +34 -0
  24. package/dist/review-card/render-leaf.d.ts +1 -1
  25. package/dist/review-card/render-leaf.js +7 -0
  26. package/dist/run/index.d.ts +25 -4
  27. package/dist/run/index.js +108 -4
  28. package/dist/run/schemas.d.ts +350 -6
  29. package/dist/run/schemas.js +95 -6
  30. package/dist/simulate/deny-cases.js +11 -0
  31. package/dist/simulate/evaluate.js +86 -5
  32. package/dist/synth/declare.d.ts +43 -0
  33. package/dist/synth/declare.js +127 -0
  34. package/dist/synth/index.d.ts +1 -0
  35. package/dist/synth/index.js +1 -0
  36. package/dist/synth/synthesize-from-recording.js +1 -1
  37. package/dist/types.d.ts +21 -1
  38. package/dist/types.js +1 -1
  39. package/dist-cjs/adapters/interpreter/adapter.d.ts +2 -2
  40. package/dist-cjs/adapters/interpreter/adapter.js +11 -3
  41. package/dist-cjs/errors.d.ts +6 -1
  42. package/dist-cjs/install/authority-overlap.d.ts +101 -0
  43. package/dist-cjs/install/authority-overlap.js +248 -0
  44. package/dist-cjs/install/build-add-context-rule.d.ts +1 -1
  45. package/dist-cjs/install/index.d.ts +1 -0
  46. package/dist-cjs/install/index.js +13 -2
  47. package/dist-cjs/install/read-account-rules.d.ts +79 -0
  48. package/dist-cjs/install/read-account-rules.js +252 -0
  49. package/dist-cjs/predicate/decode.js +22 -1
  50. package/dist-cjs/predicate/encode.js +52 -5
  51. package/dist-cjs/predicate/from-json.js +14 -1
  52. package/dist-cjs/record/index.d.ts +10 -0
  53. package/dist-cjs/record/index.js +31 -0
  54. package/dist-cjs/record/rpc.d.ts +4 -0
  55. package/dist-cjs/record/rpc.js +7 -3
  56. package/dist-cjs/registry/identify.d.ts +10 -1
  57. package/dist-cjs/registry/identify.js +4 -0
  58. package/dist-cjs/registry/on-chain-spec.d.ts +37 -0
  59. package/dist-cjs/registry/on-chain-spec.js +159 -0
  60. package/dist-cjs/review-card/builder.js +40 -0
  61. package/dist-cjs/review-card/cross-check.js +34 -0
  62. package/dist-cjs/review-card/render-leaf.d.ts +1 -1
  63. package/dist-cjs/review-card/render-leaf.js +7 -0
  64. package/dist-cjs/run/index.d.ts +25 -4
  65. package/dist-cjs/run/index.js +108 -2
  66. package/dist-cjs/run/schemas.d.ts +350 -6
  67. package/dist-cjs/run/schemas.js +96 -7
  68. package/dist-cjs/simulate/deny-cases.js +11 -0
  69. package/dist-cjs/simulate/evaluate.js +86 -5
  70. package/dist-cjs/synth/declare.d.ts +43 -0
  71. package/dist-cjs/synth/declare.js +130 -0
  72. package/dist-cjs/synth/index.d.ts +1 -0
  73. package/dist-cjs/synth/index.js +3 -1
  74. package/dist-cjs/synth/synthesize-from-recording.js +1 -1
  75. package/dist-cjs/types.d.ts +21 -1
  76. package/dist-cjs/types.js +1 -1
  77. package/package.json +1 -1
  78. package/src/adapters/interpreter/adapter.ts +13 -5
  79. package/src/errors.ts +5 -0
  80. package/src/install/authority-overlap.ts +323 -0
  81. package/src/install/index.ts +20 -0
  82. package/src/install/read-account-rules.ts +313 -0
  83. package/src/predicate/decode.ts +22 -1
  84. package/src/predicate/encode.ts +55 -5
  85. package/src/predicate/from-json.ts +14 -1
  86. package/src/record/index.ts +59 -2
  87. package/src/record/rpc.ts +4 -1
  88. package/src/registry/identify.ts +4 -1
  89. package/src/registry/on-chain-spec.ts +168 -0
  90. package/src/review-card/builder.ts +45 -2
  91. package/src/review-card/cross-check.ts +35 -1
  92. package/src/review-card/render-leaf.ts +8 -1
  93. package/src/run/index.ts +125 -2
  94. package/src/run/schemas.ts +100 -6
  95. package/src/simulate/deny-cases.ts +12 -1
  96. package/src/simulate/evaluate.ts +101 -9
  97. package/src/synth/declare.ts +206 -0
  98. package/src/synth/index.ts +5 -0
  99. package/src/synth/synthesize-from-recording.ts +1 -1
  100. 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
  }
@@ -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
- return { ok: true, data: result };
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];