@crediolabs/policy-synth 0.3.0 → 0.4.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 (152) hide show
  1. package/dist/install/authority-overlap.d.ts +101 -0
  2. package/dist/install/authority-overlap.js +227 -0
  3. package/dist/install/index.d.ts +1 -0
  4. package/dist/install/index.js +4 -0
  5. package/dist/record/freshness.d.ts +17 -11
  6. package/dist/record/freshness.js +28 -18
  7. package/dist/record/index.d.ts +10 -0
  8. package/dist/record/index.js +32 -1
  9. package/dist/record/rpc.d.ts +4 -0
  10. package/dist/record/rpc.js +4 -1
  11. package/dist/registry/identify.d.ts +10 -1
  12. package/dist/registry/identify.js +4 -1
  13. package/dist/registry/on-chain-spec.d.ts +37 -0
  14. package/dist/registry/on-chain-spec.js +152 -0
  15. package/dist/run/index.d.ts +25 -4
  16. package/dist/run/index.js +65 -4
  17. package/dist/run/schemas.d.ts +313 -0
  18. package/dist/run/schemas.js +52 -0
  19. package/dist/synth/declare.d.ts +30 -0
  20. package/dist/synth/declare.js +98 -0
  21. package/dist/synth/index.d.ts +1 -0
  22. package/dist/synth/index.js +1 -0
  23. package/dist-cjs/install/authority-overlap.d.ts +101 -0
  24. package/dist-cjs/install/authority-overlap.js +236 -0
  25. package/dist-cjs/install/index.d.ts +1 -0
  26. package/dist-cjs/install/index.js +13 -2
  27. package/dist-cjs/record/freshness.d.ts +17 -11
  28. package/dist-cjs/record/freshness.js +28 -18
  29. package/dist-cjs/record/index.d.ts +10 -0
  30. package/dist-cjs/record/index.js +31 -0
  31. package/dist-cjs/record/rpc.d.ts +4 -0
  32. package/dist-cjs/record/rpc.js +7 -3
  33. package/dist-cjs/registry/identify.d.ts +10 -1
  34. package/dist-cjs/registry/identify.js +4 -0
  35. package/dist-cjs/registry/on-chain-spec.d.ts +37 -0
  36. package/dist-cjs/registry/on-chain-spec.js +159 -0
  37. package/dist-cjs/run/index.d.ts +25 -4
  38. package/dist-cjs/run/index.js +65 -2
  39. package/dist-cjs/run/schemas.d.ts +313 -0
  40. package/dist-cjs/run/schemas.js +53 -1
  41. package/dist-cjs/synth/declare.d.ts +30 -0
  42. package/dist-cjs/synth/declare.js +101 -0
  43. package/dist-cjs/synth/index.d.ts +1 -0
  44. package/dist-cjs/synth/index.js +3 -1
  45. package/package.json +2 -2
  46. package/src/install/authority-overlap.ts +312 -0
  47. package/src/install/index.ts +20 -0
  48. package/src/record/freshness.ts +29 -18
  49. package/src/record/index.ts +59 -2
  50. package/src/record/rpc.ts +4 -1
  51. package/src/registry/identify.ts +4 -1
  52. package/src/registry/on-chain-spec.ts +168 -0
  53. package/src/run/index.ts +79 -2
  54. package/src/run/schemas.ts +57 -0
  55. package/src/synth/declare.ts +157 -0
  56. package/src/synth/index.ts +5 -0
  57. package/dist/adapters/interpreter/index.d.ts +0 -1
  58. package/dist/adapters/interpreter/index.js +0 -2
  59. package/dist/adapters/oz/adapter.d.ts +0 -20
  60. package/dist/adapters/oz/adapter.js +0 -281
  61. package/dist/adapters/oz/index.d.ts +0 -1
  62. package/dist/adapters/oz/index.js +0 -2
  63. package/dist/codegen/compile-gate.d.ts +0 -33
  64. package/dist/codegen/compile-gate.js +0 -124
  65. package/dist/codegen/index.d.ts +0 -2
  66. package/dist/codegen/index.js +0 -8
  67. package/dist/codegen/template.d.ts +0 -18
  68. package/dist/codegen/template.js +0 -148
  69. package/dist/ir/index.d.ts +0 -1
  70. package/dist/ir/index.js +0 -2
  71. package/dist/ir/types.d.ts +0 -71
  72. package/dist/ir/types.js +0 -9
  73. package/dist/mandate/index.d.ts +0 -2
  74. package/dist/mandate/index.js +0 -2
  75. package/dist/mandate/to-ir.d.ts +0 -3
  76. package/dist/mandate/to-ir.js +0 -60
  77. package/dist/mandate/types.d.ts +0 -20
  78. package/dist/mandate/types.js +0 -8
  79. package/dist/seams/index.d.ts +0 -1
  80. package/dist/seams/index.js +0 -2
  81. package/dist/seams/types.d.ts +0 -64
  82. package/dist/seams/types.js +0 -11
  83. package/dist/synth/deny-cases.d.ts +0 -28
  84. package/dist/synth/deny-cases.js +0 -212
  85. package/dist/synth/evaluate.d.ts +0 -18
  86. package/dist/synth/evaluate.js +0 -271
  87. package/dist/synth/harness.d.ts +0 -28
  88. package/dist/synth/harness.js +0 -47
  89. package/dist/synth/minimize.d.ts +0 -4
  90. package/dist/synth/minimize.js +0 -38
  91. package/dist/synth/permit-context.d.ts +0 -8
  92. package/dist/synth/permit-context.js +0 -35
  93. package/dist/synth/predicate-literals.d.ts +0 -5
  94. package/dist/synth/predicate-literals.js +0 -25
  95. package/dist/synth/synthesize-from-mandate.d.ts +0 -20
  96. package/dist/synth/synthesize-from-mandate.js +0 -59
  97. package/dist/verify/envelope.d.ts +0 -15
  98. package/dist/verify/envelope.js +0 -22
  99. package/dist/verify/index.d.ts +0 -3
  100. package/dist/verify/index.js +0 -3
  101. package/dist/verify/simulate.d.ts +0 -18
  102. package/dist/verify/simulate.js +0 -172
  103. package/dist/verify/verify.d.ts +0 -15
  104. package/dist/verify/verify.js +0 -181
  105. package/dist-cjs/adapters/interpreter/index.d.ts +0 -1
  106. package/dist-cjs/adapters/interpreter/index.js +0 -7
  107. package/dist-cjs/adapters/oz/adapter.d.ts +0 -20
  108. package/dist-cjs/adapters/oz/adapter.js +0 -286
  109. package/dist-cjs/adapters/oz/index.d.ts +0 -1
  110. package/dist-cjs/adapters/oz/index.js +0 -8
  111. package/dist-cjs/codegen/compile-gate.d.ts +0 -33
  112. package/dist-cjs/codegen/compile-gate.js +0 -128
  113. package/dist-cjs/codegen/index.d.ts +0 -2
  114. package/dist-cjs/codegen/index.js +0 -14
  115. package/dist-cjs/codegen/template.d.ts +0 -18
  116. package/dist-cjs/codegen/template.js +0 -151
  117. package/dist-cjs/ir/index.d.ts +0 -1
  118. package/dist-cjs/ir/index.js +0 -3
  119. package/dist-cjs/ir/types.d.ts +0 -71
  120. package/dist-cjs/ir/types.js +0 -10
  121. package/dist-cjs/mandate/index.d.ts +0 -2
  122. package/dist-cjs/mandate/index.js +0 -6
  123. package/dist-cjs/mandate/to-ir.d.ts +0 -3
  124. package/dist-cjs/mandate/to-ir.js +0 -63
  125. package/dist-cjs/mandate/types.d.ts +0 -20
  126. package/dist-cjs/mandate/types.js +0 -9
  127. package/dist-cjs/seams/index.d.ts +0 -1
  128. package/dist-cjs/seams/index.js +0 -3
  129. package/dist-cjs/seams/types.d.ts +0 -64
  130. package/dist-cjs/seams/types.js +0 -12
  131. package/dist-cjs/synth/deny-cases.d.ts +0 -28
  132. package/dist-cjs/synth/deny-cases.js +0 -217
  133. package/dist-cjs/synth/evaluate.d.ts +0 -18
  134. package/dist-cjs/synth/evaluate.js +0 -274
  135. package/dist-cjs/synth/harness.d.ts +0 -28
  136. package/dist-cjs/synth/harness.js +0 -50
  137. package/dist-cjs/synth/minimize.d.ts +0 -4
  138. package/dist-cjs/synth/minimize.js +0 -41
  139. package/dist-cjs/synth/permit-context.d.ts +0 -8
  140. package/dist-cjs/synth/permit-context.js +0 -38
  141. package/dist-cjs/synth/predicate-literals.d.ts +0 -5
  142. package/dist-cjs/synth/predicate-literals.js +0 -28
  143. package/dist-cjs/synth/synthesize-from-mandate.d.ts +0 -20
  144. package/dist-cjs/synth/synthesize-from-mandate.js +0 -62
  145. package/dist-cjs/verify/envelope.d.ts +0 -15
  146. package/dist-cjs/verify/envelope.js +0 -23
  147. package/dist-cjs/verify/index.d.ts +0 -3
  148. package/dist-cjs/verify/index.js +0 -8
  149. package/dist-cjs/verify/simulate.d.ts +0 -18
  150. package/dist-cjs/verify/simulate.js +0 -175
  151. package/dist-cjs/verify/verify.d.ts +0 -15
  152. package/dist-cjs/verify/verify.js +0 -184
@@ -44,17 +44,23 @@ function isBelowThreshold(c) {
44
44
  * under-confidence recording.
45
45
  *
46
46
  * The remediation text branches on which diagnostic bucket is non-empty:
47
- * - unknownContracts only -> user MUST supply an ABI (or re-capture
48
- * against a known protocol version). The contract is real, the
49
- * recorder just cannot decode it.
50
- * - opaqueScVals only -> the recorder encountered a value shape it
51
- * should support but did not decode. The user cannot supply an ABI
52
- * to fix a decoder bug; the guidance explicitly says so and points at
53
- * re-running after a tool upgrade / reporting the path.
54
- * - both -> list both diagnostics AND chain the right remediation for
55
- * each (the user supplies an ABI AND reports the decoder gap).
56
- * - neither (the "denom === 0" path) -> unchanged from the pre-fix
57
- * text; the user did not actually hit a code-level barrier. */
47
+ * - unknownContracts only -> the contract is not in the compiled-in
48
+ * registry. NOT "the contract has no interface": most Soroban
49
+ * contracts publish a typed spec on chain, this package just never
50
+ * reads one. The reason code stays `no-abi` because it is a published
51
+ * enum value, so the TEXT has to carry the correction.
52
+ * - opaqueScVals only -> the recorder met a value shape it should
53
+ * support but did not decode. That is a decoder bug, and the guidance
54
+ * says so rather than implying the caller can fix it.
55
+ * - both -> list both diagnostics and chain the right remediation.
56
+ * - neither (the "denom === 0" path) -> the caller did not actually hit
57
+ * a code-level barrier.
58
+ *
59
+ * Do NOT tell the caller to supply an ABI. There is no input for one:
60
+ * recognition runs off the compiled-in registry plus a known-contract set
61
+ * that no public tool boundary exposes. The old text named an action
62
+ * nobody could take and sent users looking for a file they do not have and
63
+ * would not need. */
58
64
  function buildLowConfidenceQuestion(c) {
59
65
  const hasUnknown = c.unknownContracts.length > 0;
60
66
  const hasOpaque = c.opaqueScVals.length > 0;
@@ -67,16 +73,20 @@ function buildLowConfidenceQuestion(c) {
67
73
  }
68
74
  const why = reasons.length === 0 ? 'no diagnostic reason available' : reasons.join('; ');
69
75
  const header = `Recording refused: parseConfidence ${c.overall.toFixed(3)} is below the threshold ${c.thresholdUsed.toFixed(3)}. Diagnostic: ${why}.`;
70
- // The two remediation branches are explicit so the user (or the agent)
71
- // can dispatch on the verb. The "unknown contract" branch is the only
72
- // one that asks for an ABI; the "opaque ScVal" branch asks the user to
73
- // report the path and re-run after a tool upgrade.
76
+ // The remediation branches are explicit so the user (or the agent) can
77
+ // dispatch on the verb. Neither branch asks for an ABI: there is no input
78
+ // that accepts one, and for the unknown-contract branch the contract's
79
+ // own spec is usually on chain already - it is this package that does not
80
+ // read it.
81
+ const unknownAdvice = `The contract is not in this package's built-in registry - "no-abi" means unrecognised here, not that the contract lacks an interface. ` +
82
+ `You cannot supply one: the registry is compiled in. Either build the policy without deriving it from this transaction, or re-capture against a protocol the registry covers. ` +
83
+ `Passing confidenceOverride accepts the recording as it stands, including the arguments this tool could not attribute - do that only if you have checked them yourself.`;
74
84
  if (hasUnknown && hasOpaque) {
75
- return (`${header} Supply an ABI for the unknown contract(s) or re-capture the transaction against a known protocol version; ` +
76
- `also file the opaque ScVal path against the recorder (decoder limitation) and re-run record_transaction after a tool upgrade.`);
85
+ return (`${header} ${unknownAdvice} Separately, file the opaque ScVal path against the recorder (a decoder limitation) ` +
86
+ `and re-run record_transaction after a tool upgrade.`);
77
87
  }
78
88
  if (hasUnknown) {
79
- return `${header} Supply an ABI for the unknown contract(s) or re-capture the transaction against a known protocol version, then re-run record_transaction.`;
89
+ return `${header} ${unknownAdvice}`;
80
90
  }
81
91
  if (hasOpaque) {
82
92
  return `${header} This is a recorder decoder limitation (the value shape is supported in principle but not yet decoded) - the agent cannot fix it by supplying an ABI. Report the opaque ScVal path above against the recorder and re-run record_transaction after a tool upgrade.`;
@@ -1,4 +1,5 @@
1
1
  import type { ToolResponse } from '../errors.ts';
2
+ import { type SpecFetcher } from '../registry/on-chain-spec.ts';
2
3
  import type { Network, RecordedTransaction } from '../types.ts';
3
4
  import { type RpcFetcher } from './rpc.ts';
4
5
  /** Public input shape. The brief pins:
@@ -27,6 +28,15 @@ export interface RecordInput {
27
28
  * automatically; tests can pass a deterministic stub. */
28
29
  crossNetworkFetcher?: RpcFetcher;
29
30
  confidenceOverride?: number;
31
+ /** Read a contract's own interface off chain when the compiled-in registry
32
+ * does not recognise it. Default ON: the registry covers the protocols we
33
+ * pinned by hand, and refusing everything else reported `no-abi` for
34
+ * contracts that publish a full typed spec. Set false to record against
35
+ * the registry alone (no extra RPC). */
36
+ resolveContractSpecs?: boolean;
37
+ /** Test seam for the spec lookup. Unset in production, where it is built
38
+ * from the network's pinned RPC URL. */
39
+ specFetcher?: SpecFetcher;
30
40
  }
31
41
  export type RecordResult = ToolResponse<RecordedTransaction>;
32
42
  export declare function recordTransaction(input: RecordInput): Promise<RecordResult>;
@@ -19,11 +19,40 @@
19
19
  // defined in src/errors.ts.
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
21
  exports.recordTransaction = recordTransaction;
22
+ const stellar_sdk_1 = require("@stellar/stellar-sdk");
23
+ const on_chain_spec_ts_1 = require("../registry/on-chain-spec.js");
22
24
  const decode_ts_1 = require("./decode.js");
23
25
  const freshness_ts_1 = require("./freshness.js");
24
26
  const movements_ts_1 = require("./movements.js");
25
27
  const rpc_ts_1 = require("./rpc.js");
26
28
  const validate_ts_1 = require("./validate.js");
29
+ /** Second pass over the contracts the compiled-in registry did not recognise.
30
+ *
31
+ * Each candidate's own interface is read off chain and every call it received
32
+ * is checked against it; the ones that verify are fed back through the decoder
33
+ * as known. Re-decoding rather than patching the first result keeps ONE code
34
+ * path computing parseConfidence - a hand-adjusted count here would be a
35
+ * second implementation of the gate, free to drift from the real one.
36
+ *
37
+ * Only ever ADDS recognition. A missing spec, an unreachable RPC or a call the
38
+ * interface does not describe all leave the recording exactly as it was. */
39
+ async function resolveByOnChainSpec(input, decoded, redecode) {
40
+ if (input.resolveContractSpecs === false)
41
+ return decoded;
42
+ if (decoded.unknownContracts.length === 0)
43
+ return decoded;
44
+ const fetcher = input.specFetcher ??
45
+ (0, on_chain_spec_ts_1.specFetcherFromRpc)(rpc_ts_1.PUBLIC_RPC_URLS[input.network], input.network === 'mainnet' ? stellar_sdk_1.Networks.PUBLIC : stellar_sdk_1.Networks.TESTNET);
46
+ let resolved;
47
+ try {
48
+ resolved = await (0, on_chain_spec_ts_1.resolveContractsByOnChainSpec)(decoded.invocations, decoded.unknownContracts.map((u) => u.contract), fetcher);
49
+ }
50
+ catch {
51
+ // A lookup failure must not fail the recording that already succeeded.
52
+ return decoded;
53
+ }
54
+ return resolved.size === 0 ? decoded : redecode(resolved);
55
+ }
27
56
  async function recordTransaction(input) {
28
57
  if (!input.network) {
29
58
  return err('RECORDING_FAILED', 'network required', false);
@@ -79,6 +108,7 @@ async function recordTransaction(input) {
79
108
  return err('RECORDING_FAILED', e.message, false);
80
109
  throw e;
81
110
  }
111
+ decoded = await resolveByOnChainSpec(input, decoded, (known) => (0, decode_ts_1.decodeEnvelope)(fetched.envelopeXdr, events, [], fetched.ledger, known, input.network));
82
112
  return finish(input.network, decoded, input.confidenceOverride);
83
113
  }
84
114
  // XDR mode has no raw on-chain events, so the events-based cross-check is
@@ -96,6 +126,7 @@ async function recordTransaction(input) {
96
126
  return err('RECORDING_FAILED', e.message, false);
97
127
  return err('RECORDING_FAILED', `failed to decode base64 envelope XDR: ${e.message}`, false);
98
128
  }
129
+ decoded = await resolveByOnChainSpec(input, decoded, (known) => (0, decode_ts_1.decodeEnvelopeXdr)(xdrStr, [], [], 0, known, input.network));
99
130
  return finish(input.network, decoded, input.confidenceOverride);
100
131
  }
101
132
  function combineEvents(raw) {
@@ -14,6 +14,10 @@ export interface SorobanTxResponse {
14
14
  events: rpc.Api.TransactionEvents;
15
15
  }
16
16
  export type RpcFetcher = (hash: string) => Promise<SorobanTxResponse | null>;
17
+ /** Exported so the on-chain spec lookup reads from the SAME endpoint the
18
+ * recorder fetched the transaction from. Two different endpoints could
19
+ * disagree about what a contract is. */
20
+ export declare const PUBLIC_RPC_URLS: Record<Network, string>;
17
21
  /** Build a fetcher backed by the public Soroban RPC for the given network.
18
22
  * Injectable by tests via the `fetcher` parameter to `recordTransaction`. */
19
23
  export declare function createRpcServer(network: Network): RpcFetcher;
@@ -5,10 +5,14 @@
5
5
  // Unit tests inject a fetcher so they never hit the network; the integration test
6
6
  // uses the real Soroban RPC through `createRpcServer`.
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.PUBLIC_RPC_URLS = void 0;
8
9
  exports.createRpcServer = createRpcServer;
9
10
  exports.probeNetwork = probeNetwork;
10
11
  const stellar_sdk_1 = require("@stellar/stellar-sdk");
11
- const PUBLIC_RPC_URLS = {
12
+ /** Exported so the on-chain spec lookup reads from the SAME endpoint the
13
+ * recorder fetched the transaction from. Two different endpoints could
14
+ * disagree about what a contract is. */
15
+ exports.PUBLIC_RPC_URLS = {
12
16
  testnet: 'https://soroban-testnet.stellar.org',
13
17
  // The brief pins testnet; mainnet is left to the caller via injection. We keep
14
18
  // a public default that matches the brief's note ("e.g. https://mainnet.sorobanrpc.com").
@@ -17,7 +21,7 @@ const PUBLIC_RPC_URLS = {
17
21
  /** Build a fetcher backed by the public Soroban RPC for the given network.
18
22
  * Injectable by tests via the `fetcher` parameter to `recordTransaction`. */
19
23
  function createRpcServer(network) {
20
- const server = new stellar_sdk_1.rpc.Server(PUBLIC_RPC_URLS[network], { allowHttp: false });
24
+ const server = new stellar_sdk_1.rpc.Server(exports.PUBLIC_RPC_URLS[network], { allowHttp: false });
21
25
  return async (hash) => {
22
26
  const resp = await server.getTransaction(hash);
23
27
  if (resp.status === stellar_sdk_1.rpc.Api.GetTransactionStatus.NOT_FOUND) {
@@ -50,7 +54,7 @@ function createRpcServer(network) {
50
54
  /** Lightweight reachability probe used by the integration test to decide whether
51
55
  * to run or self-skip. Sends a JSON-RPC `getHealth` to the public endpoint. */
52
56
  async function probeNetwork(network, timeoutMs = 3000) {
53
- const url = PUBLIC_RPC_URLS[network];
57
+ const url = exports.PUBLIC_RPC_URLS[network];
54
58
  const controller = new AbortController();
55
59
  const timer = setTimeout(() => controller.abort(), timeoutMs);
56
60
  try {
@@ -1,5 +1,5 @@
1
1
  import type { ScVal } from '../types.ts';
2
- import type { ProtocolId } from './protocols.ts';
2
+ import type { AbiArg, ProtocolId } from './protocols.ts';
3
3
  export interface IdentifiedProtocol {
4
4
  protocol: ProtocolId;
5
5
  fn: string;
@@ -9,3 +9,12 @@ export interface IdentifiedProtocol {
9
9
  * off, or unknown method on a pinned address). The caller MUST keep the
10
10
  * null path fail-closed. */
11
11
  export declare function identifyProtocol(contract: string, method: string, args: ScVal[], network?: 'mainnet' | 'testnet'): IdentifiedProtocol | null;
12
+ /** Compare the decoded args against the ABI signature. Returns true only when
13
+ * the arg count matches AND every arg's ScVal subset type matches the ABI's
14
+ * declared type. `other` is intentionally NOT a valid ABI match - it means
15
+ * the decoder couldn't classify the value, which is exactly the signal
16
+ * fail-closed should refuse. */
17
+ /** Exported so the on-chain-spec path checks a call the SAME way a pinned
18
+ * protocol does. Reimplementing it there would let the two drift, and a
19
+ * looser copy would be the fail-OPEN direction. */
20
+ export declare function argsMatchAbi(expected: AbiArg[], actual: ScVal[]): boolean;
@@ -31,6 +31,7 @@
31
31
  // ScVal subset type against the ABI signature; mismatch -> null.
32
32
  Object.defineProperty(exports, "__esModule", { value: true });
33
33
  exports.identifyProtocol = identifyProtocol;
34
+ exports.argsMatchAbi = argsMatchAbi;
34
35
  const known_addresses_ts_1 = require("./known-addresses.js");
35
36
  const protocols_ts_1 = require("./protocols.js");
36
37
  /** Identify the protocol for a single (contract, method, args) invocation.
@@ -91,6 +92,9 @@ function identifyProtocol(contract, method, args, network) {
91
92
  * declared type. `other` is intentionally NOT a valid ABI match - it means
92
93
  * the decoder couldn't classify the value, which is exactly the signal
93
94
  * fail-closed should refuse. */
95
+ /** Exported so the on-chain-spec path checks a call the SAME way a pinned
96
+ * protocol does. Reimplementing it there would let the two drift, and a
97
+ * looser copy would be the fail-OPEN direction. */
94
98
  function argsMatchAbi(expected, actual) {
95
99
  if (expected.length !== actual.length)
96
100
  return false;
@@ -0,0 +1,37 @@
1
+ import { contract as sdkContract } from '@stellar/stellar-sdk';
2
+ import type { ContractInvocation } from '../types.ts';
3
+ import type { AbiArgType, ProtocolAbi } from './protocols.ts';
4
+ /** Map an XDR spec type to the ScVal subset vocabulary the matcher uses.
5
+ *
6
+ * Returns null for a type the recorder's ScVal subset cannot represent. That
7
+ * is deliberate: an argument we cannot type is an argument we cannot check,
8
+ * and claiming a match on it would be the fail-OPEN direction. A function
9
+ * with any such argument is dropped from the derived ABI, so a call to it
10
+ * stays unrecognised rather than being waved through. */
11
+ export declare function abiTypeFromSpecType(specTypeName: string): AbiArgType | null;
12
+ /** The RPC surface this module needs. Narrowed to one method so a test can
13
+ * supply a stub without standing up a server, and so the recorder's existing
14
+ * RPC client can be passed straight in. */
15
+ export interface SpecFetcher {
16
+ contractSpec(contractId: string): Promise<sdkContract.Spec | null>;
17
+ }
18
+ /** Build a `SpecFetcher` over the SDK's contract client. */
19
+ export declare function specFetcherFromRpc(rpcUrl: string, networkPassphrase: string): SpecFetcher;
20
+ /** Convert a fetched spec into the ABI shape `identifyProtocol` already
21
+ * matches against. Functions with an argument outside the ScVal subset are
22
+ * OMITTED rather than partially typed. */
23
+ export declare function abiFromSpec(spec: sdkContract.Spec): ProtocolAbi;
24
+ /** Fetch and convert in one step. Returns null when no usable interface was
25
+ * obtained, which the caller must treat as "unrecognised". */
26
+ export declare function fetchContractAbi(contractId: string, fetcher: SpecFetcher): Promise<ProtocolAbi | null>;
27
+ /** Contracts whose EVERY recorded call matches their own published interface.
28
+ *
29
+ * All-or-nothing per contract, deliberately. A contract where one call
30
+ * verifies and another does not is a contract we do not understand, and
31
+ * marking it recognised would raise confidence on the strength of the call
32
+ * we happened to check. The unverified call is the one that matters.
33
+ *
34
+ * A contract with no fetchable spec, or a call naming a function absent from
35
+ * it, simply stays unknown - this only ever ADDS recognition, so a failure
36
+ * here degrades to today's behaviour rather than to a wrong answer. */
37
+ export declare function resolveContractsByOnChainSpec(invocations: ReadonlyArray<ContractInvocation>, candidates: ReadonlyArray<string>, fetcher: SpecFetcher): Promise<Set<string>>;
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ // src/registry/on-chain-spec.ts - read a contract's own interface off chain.
3
+ //
4
+ // The compiled-in registry covers the protocols we pinned by hand. Everything
5
+ // else was reported as `no-abi`, which read as "this contract does not
6
+ // describe itself" when it almost always does: a Soroban contract embeds a
7
+ // typed spec in its wasm, and the network will hand it over. The recorder was
8
+ // refusing calls whose interface was one RPC round-trip away.
9
+ //
10
+ // What this buys is NARROW and deliberately so. A fetched spec says what the
11
+ // contract's arguments ARE, not what they MEAN. So a call verified against it
12
+ // is recognised - the decode is trustworthy, the confidence gate stops
13
+ // refusing it - but the arguments carry the contract's own parameter NAMES
14
+ // rather than a curated meaning, and nothing here infers which argument is a
15
+ // spend or a recipient. A pinned protocol still outranks a fetched spec for
16
+ // exactly that reason, and `identifyProtocol` is consulted first.
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.abiTypeFromSpecType = abiTypeFromSpecType;
19
+ exports.specFetcherFromRpc = specFetcherFromRpc;
20
+ exports.abiFromSpec = abiFromSpec;
21
+ exports.fetchContractAbi = fetchContractAbi;
22
+ exports.resolveContractsByOnChainSpec = resolveContractsByOnChainSpec;
23
+ const stellar_sdk_1 = require("@stellar/stellar-sdk");
24
+ const identify_ts_1 = require("./identify.js");
25
+ /** Map an XDR spec type to the ScVal subset vocabulary the matcher uses.
26
+ *
27
+ * Returns null for a type the recorder's ScVal subset cannot represent. That
28
+ * is deliberate: an argument we cannot type is an argument we cannot check,
29
+ * and claiming a match on it would be the fail-OPEN direction. A function
30
+ * with any such argument is dropped from the derived ABI, so a call to it
31
+ * stays unrecognised rather than being waved through. */
32
+ function abiTypeFromSpecType(specTypeName) {
33
+ switch (specTypeName) {
34
+ case 'scSpecTypeAddress':
35
+ return 'address';
36
+ case 'scSpecTypeI128':
37
+ return 'i128';
38
+ case 'scSpecTypeU64':
39
+ case 'scSpecTypeI64':
40
+ return 'u64';
41
+ case 'scSpecTypeU32':
42
+ case 'scSpecTypeI32':
43
+ return 'u32';
44
+ case 'scSpecTypeSymbol':
45
+ return 'symbol';
46
+ case 'scSpecTypeVec':
47
+ return 'vec';
48
+ case 'scSpecTypeBytes':
49
+ case 'scSpecTypeBytesN':
50
+ return 'bytes';
51
+ case 'scSpecTypeMap':
52
+ return 'map';
53
+ default:
54
+ return null;
55
+ }
56
+ }
57
+ /** Build a `SpecFetcher` over the SDK's contract client. */
58
+ function specFetcherFromRpc(rpcUrl, networkPassphrase) {
59
+ return {
60
+ async contractSpec(contractId) {
61
+ try {
62
+ const client = await stellar_sdk_1.contract.Client.from({ contractId, networkPassphrase, rpcUrl });
63
+ return client.spec ?? null;
64
+ }
65
+ catch {
66
+ // A contract with no spec, an unreachable RPC and a bad address all
67
+ // land here and all mean the same thing to the caller: no interface
68
+ // was obtained, so the contract stays unrecognised. Fail closed.
69
+ return null;
70
+ }
71
+ },
72
+ };
73
+ }
74
+ /** Convert a fetched spec into the ABI shape `identifyProtocol` already
75
+ * matches against. Functions with an argument outside the ScVal subset are
76
+ * OMITTED rather than partially typed. */
77
+ function abiFromSpec(spec) {
78
+ const abi = {};
79
+ for (const fn of spec.funcs()) {
80
+ const name = fn.name().toString();
81
+ // The constructor is not callable after deployment, so a recorded
82
+ // invocation can never be one. Keeping it would only widen the surface.
83
+ if (name === '__constructor')
84
+ continue;
85
+ const args = [];
86
+ let usable = true;
87
+ for (const input of fn.inputs()) {
88
+ const type = abiTypeFromSpecType(input.type().switch().name);
89
+ if (type === null) {
90
+ usable = false;
91
+ break;
92
+ }
93
+ const argName = input.name().toString();
94
+ args.push({
95
+ name: argName,
96
+ type,
97
+ // The contract's own parameter name is the honest description. It is
98
+ // NOT a curated meaning - nothing here knows whether `value` is a
99
+ // spend - so downstream must not treat it as one.
100
+ meaning: `${argName} (from the contract's on-chain interface)`,
101
+ });
102
+ }
103
+ if (usable)
104
+ abi[name] = { args };
105
+ }
106
+ return abi;
107
+ }
108
+ /** Fetch and convert in one step. Returns null when no usable interface was
109
+ * obtained, which the caller must treat as "unrecognised". */
110
+ async function fetchContractAbi(contractId, fetcher) {
111
+ const spec = await fetcher.contractSpec(contractId);
112
+ if (spec === null)
113
+ return null;
114
+ const abi = abiFromSpec(spec);
115
+ return Object.keys(abi).length === 0 ? null : abi;
116
+ }
117
+ /** Every contract invoked anywhere in the tree, with the calls made on it. */
118
+ function callsByContract(invocations) {
119
+ const out = new Map();
120
+ const walk = (inv) => {
121
+ const list = out.get(inv.contract) ?? [];
122
+ list.push({ fn: inv.fn, args: inv.args });
123
+ out.set(inv.contract, list);
124
+ for (const sub of inv.subInvocations)
125
+ walk(sub);
126
+ };
127
+ for (const inv of invocations)
128
+ walk(inv);
129
+ return out;
130
+ }
131
+ /** Contracts whose EVERY recorded call matches their own published interface.
132
+ *
133
+ * All-or-nothing per contract, deliberately. A contract where one call
134
+ * verifies and another does not is a contract we do not understand, and
135
+ * marking it recognised would raise confidence on the strength of the call
136
+ * we happened to check. The unverified call is the one that matters.
137
+ *
138
+ * A contract with no fetchable spec, or a call naming a function absent from
139
+ * it, simply stays unknown - this only ever ADDS recognition, so a failure
140
+ * here degrades to today's behaviour rather than to a wrong answer. */
141
+ async function resolveContractsByOnChainSpec(invocations, candidates, fetcher) {
142
+ const resolved = new Set();
143
+ const calls = callsByContract(invocations);
144
+ for (const contract of new Set(candidates)) {
145
+ const made = calls.get(contract);
146
+ if (!made || made.length === 0)
147
+ continue;
148
+ const abi = await fetchContractAbi(contract, fetcher);
149
+ if (abi === null)
150
+ continue;
151
+ const everyCallVerifies = made.every((c) => {
152
+ const entry = abi[c.fn];
153
+ return entry !== undefined && (0, identify_ts_1.argsMatchAbi)(entry.args, c.args);
154
+ });
155
+ if (everyCallVerifies)
156
+ resolved.add(contract);
157
+ }
158
+ return resolved;
159
+ }
@@ -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
  *
@@ -18,20 +18,23 @@
18
18
  // No business logic. No retries. No session state. The same call shape can
19
19
  // drive the CLI (which calls into the same core directly without MCP).
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
- exports.ToolErrorSchema = exports.TESTNET_RPC_URL = exports.SynthesizePolicyInputSchema = exports.RPC_URL_BY_NETWORK = exports.RevokePolicyInputSchema = exports.RecordTransactionInputSchema = exports.RecordedTransactionSchema = exports.PredicateNodeSchema = exports.PredicateLeafSchema = exports.PINNED_INTERPRETER_WASM_SHA256 = exports.PINNED_INTERPRETER_TESTNET_ADDRESS = exports.PINNED_INTERPRETER_MAINNET_ADDRESS = exports.PINNED_INTERPRETER_GRAMMAR_VERSION = exports.PINNED_INTERPRETER_ADDRESS_BY_NETWORK = exports.NetworkSchema = exports.MAINNET_RPC_URL = exports.InterpreterOptionsSchema = exports.InstallPolicyInputSchema = exports.GetInterpreterInfoInputSchema = exports.ComposeUserResponsesSchema = void 0;
21
+ exports.ToolErrorSchema = exports.TESTNET_RPC_URL = exports.SynthesizePolicyInputSchema = exports.RPC_URL_BY_NETWORK = exports.RevokePolicyInputSchema = exports.RecordTransactionInputSchema = exports.RecordedTransactionSchema = exports.PredicateNodeSchema = exports.PredicateLeafSchema = exports.PINNED_INTERPRETER_WASM_SHA256 = exports.PINNED_INTERPRETER_TESTNET_ADDRESS = exports.PINNED_INTERPRETER_MAINNET_ADDRESS = exports.PINNED_INTERPRETER_GRAMMAR_VERSION = exports.PINNED_INTERPRETER_ADDRESS_BY_NETWORK = exports.NetworkSchema = exports.MAINNET_RPC_URL = exports.InterpreterOptionsSchema = exports.InstallPolicyInputSchema = exports.GetInterpreterInfoInputSchema = exports.DeclarePolicyInputSchema = exports.ComposeUserResponsesSchema = void 0;
22
22
  exports.runRecordTransaction = runRecordTransaction;
23
23
  exports.runSynthesizePolicy = runSynthesizePolicy;
24
24
  exports.runInstallPolicy = runInstallPolicy;
25
25
  exports.runRevokePolicy = runRevokePolicy;
26
26
  exports.runSimulatePolicy = runSimulatePolicy;
27
+ exports.runDeclarePolicy = runDeclarePolicy;
27
28
  exports.runVerifyPolicy = runVerifyPolicy;
28
29
  exports.runGetInterpreterInfo = runGetInterpreterInfo;
29
30
  exports.caughtError = caughtError;
30
31
  const node_crypto_1 = require("node:crypto");
31
32
  const stellar_sdk_1 = require("@stellar/stellar-sdk");
32
33
  const index_ts_1 = require("../index.js");
34
+ const authority_overlap_ts_1 = require("../install/authority-overlap.js");
33
35
  const build_install_policy_ts_1 = require("../install/build-install-policy.js");
34
36
  const get_interpreter_info_ts_1 = require("../install/get-interpreter-info.js");
37
+ const decode_ts_1 = require("../predicate/decode.js");
35
38
  const index_ts_2 = require("../simulate/index.js");
36
39
  const schemas_ts_1 = require("./schemas.js");
37
40
  // Re-export the underlying Zod schemas so the MCP package (and any other
@@ -40,6 +43,7 @@ const schemas_ts_1 = require("./schemas.js");
40
43
  // truth - MCP tool shapes are derived from them.
41
44
  var schemas_ts_2 = require("./schemas.js");
42
45
  Object.defineProperty(exports, "ComposeUserResponsesSchema", { enumerable: true, get: function () { return schemas_ts_2.ComposeUserResponsesSchema; } });
46
+ Object.defineProperty(exports, "DeclarePolicyInputSchema", { enumerable: true, get: function () { return schemas_ts_2.DeclarePolicyInputSchema; } });
43
47
  Object.defineProperty(exports, "GetInterpreterInfoInputSchema", { enumerable: true, get: function () { return schemas_ts_2.GetInterpreterInfoInputSchema; } });
44
48
  Object.defineProperty(exports, "InstallPolicyInputSchema", { enumerable: true, get: function () { return schemas_ts_2.InstallPolicyInputSchema; } });
45
49
  Object.defineProperty(exports, "InterpreterOptionsSchema", { enumerable: true, get: function () { return schemas_ts_2.InterpreterOptionsSchema; } });
@@ -65,6 +69,7 @@ Object.defineProperty(exports, "ToolErrorSchema", { enumerable: true, get: funct
65
69
  const TOOL_ERROR_CODE = {
66
70
  record_transaction: 'RECORDING_FAILED',
67
71
  synthesize_policy: 'SYNTHESIS_ERROR',
72
+ declare_policy: 'SYNTHESIS_ERROR',
68
73
  simulate_policy: 'SIMULATION_ERROR',
69
74
  verify_policy: 'VERIFICATION_FAILED',
70
75
  install_policy: 'INSTALL_BUILD_FAILED',
@@ -179,7 +184,27 @@ async function runInstallPolicy(raw) {
179
184
  rpc: rpcClient,
180
185
  ...(input.baseFee !== undefined ? { baseFee: input.baseFee } : {}),
181
186
  });
182
- return { ok: true, data: result };
187
+ // Cross-rule scan, when the caller supplied what else is on the account.
188
+ // ABSENT is reported as `null` rather than an empty list: "we did not
189
+ // look" and "we looked and found nothing" are different answers, and
190
+ // collapsing them would let a caller read silence as safety.
191
+ const authorityScan = input.existingRules === undefined
192
+ ? null
193
+ : (0, authority_overlap_ts_1.findAuthorityOverlaps)({
194
+ intended: {
195
+ // `add_context_rule` gets its id FROM the account, so there is
196
+ // no existing rule this install replaces. A sentinel no real id
197
+ // can equal keeps every observed rule in scope.
198
+ ruleId: -1,
199
+ contextType: input.rule.contextRuleType,
200
+ signers: input.rule.signers,
201
+ predicate: (0, decode_ts_1.decodePredicate)(encodedPredicate),
202
+ },
203
+ // The schema types `predicate` loosely (it is the shared
204
+ // PredicateNodeSchema); the shape is already validated.
205
+ existing: input.existingRules,
206
+ });
207
+ return { ok: true, data: { ...result, authorityScan } };
183
208
  }
184
209
  catch (e) {
185
210
  return toolFailure('install_policy', e);
@@ -298,6 +323,44 @@ function runSimulatePolicy(raw) {
298
323
  return toolFailure('simulate_policy', e);
299
324
  }
300
325
  }
326
+ /** `declare_policy` body - the DECLARATIVE front-end.
327
+ *
328
+ * `synthesize_policy` infers a predicate from a transaction that happened;
329
+ * this takes the constraint stated outright. No RPC, no decoding and no
330
+ * parseConfidence, so nothing here can be refused for a contract the registry
331
+ * does not recognise - which is most of the point of having it.
332
+ *
333
+ * The returned `warnings` are load-bearing, not decoration. An argument index
334
+ * the caller did not supply is DEFAULTED to the SEP-41 position, and a bound
335
+ * on the wrong argument constrains something the caller did not mean without
336
+ * ever announcing itself, so a caller that ignores warnings can install a
337
+ * predicate that reads correctly and binds nothing. */
338
+ function runDeclarePolicy(raw) {
339
+ const parsed = schemas_ts_1.DeclarePolicyInputSchema.safeParse(raw);
340
+ if (!parsed.success) {
341
+ return { ok: false, error: validationError('declare_policy', parsed.error.issues) };
342
+ }
343
+ try {
344
+ // Rebuilt field-by-field rather than passed through: the schema's
345
+ // optionals are `T | undefined` and `PolicyDeclaration`'s are absent-or-T,
346
+ // which `exactOptionalPropertyTypes` treats as different.
347
+ const d = parsed.data;
348
+ const { predicate, warnings } = (0, index_ts_1.declarePredicate)({
349
+ fn: d.fn,
350
+ ...(d.contract !== undefined ? { contract: d.contract } : {}),
351
+ ...(d.maxAmount !== undefined ? { maxAmount: d.maxAmount } : {}),
352
+ ...(d.amountArgIndex !== undefined ? { amountArgIndex: d.amountArgIndex } : {}),
353
+ ...(d.recipients !== undefined ? { recipients: d.recipients } : {}),
354
+ ...(d.recipientArgIndex !== undefined ? { recipientArgIndex: d.recipientArgIndex } : {}),
355
+ ...(d.allowZeroCap !== undefined ? { allowZeroCap: d.allowZeroCap } : {}),
356
+ });
357
+ const { encodedPredicate, predicateHash } = (0, index_ts_1.encodePredicate)(predicate);
358
+ return { ok: true, data: { predicate, encodedPredicate, predicateHash, warnings } };
359
+ }
360
+ catch (e) {
361
+ return toolFailure('declare_policy', e);
362
+ }
363
+ }
301
364
  /** `verify_policy` body - the permit case plus a generated deny case per
302
365
  * dimension.
303
366
  *