@crediolabs/policy-synth 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/LICENSE +21 -0
  2. package/dist/codegen/template.js +3 -3
  3. package/dist/record/decode.d.ts +6 -3
  4. package/dist/record/decode.js +56 -3
  5. package/dist/record/freshness.d.ts +14 -1
  6. package/dist/record/freshness.js +32 -2
  7. package/dist/record/index.d.ts +11 -0
  8. package/dist/record/index.js +25 -0
  9. package/dist/record/movements.d.ts +15 -3
  10. package/dist/record/movements.js +42 -7
  11. package/dist/run/index.d.ts +43 -0
  12. package/dist/run/index.js +222 -0
  13. package/dist/run/schemas.d.ts +2016 -0
  14. package/dist/run/schemas.js +223 -0
  15. package/dist/synth/address.d.ts +7 -0
  16. package/dist/synth/address.js +12 -0
  17. package/dist/synth/compose-from-recording.d.ts +4 -3
  18. package/dist/synth/compose-from-recording.js +16 -6
  19. package/dist/synth/deny-cases.d.ts +12 -2
  20. package/dist/synth/deny-cases.js +76 -4
  21. package/dist/synth/evaluate.js +2 -2
  22. package/dist/synth/index.d.ts +1 -0
  23. package/dist/synth/index.js +4 -0
  24. package/dist/synth/minimize.d.ts +1 -1
  25. package/dist/synth/minimize.js +3 -3
  26. package/dist/synth/synthesize-from-recording.d.ts +5 -2
  27. package/dist/synth/synthesize-from-recording.js +175 -69
  28. package/dist/types.d.ts +41 -1
  29. package/dist/types.js +17 -0
  30. package/dist/verify/simulate.js +18 -2
  31. package/dist/verify/verify.js +18 -2
  32. package/dist-cjs/adapters/interpreter/adapter.d.ts +38 -0
  33. package/dist-cjs/adapters/interpreter/adapter.js +527 -0
  34. package/dist-cjs/adapters/interpreter/index.d.ts +1 -0
  35. package/dist-cjs/adapters/interpreter/index.js +8 -0
  36. package/dist-cjs/adapters/oz/adapter.d.ts +20 -0
  37. package/dist-cjs/adapters/oz/adapter.js +289 -0
  38. package/dist-cjs/adapters/oz/index.d.ts +1 -0
  39. package/dist-cjs/adapters/oz/index.js +8 -0
  40. package/dist-cjs/codegen/compile-gate.d.ts +33 -0
  41. package/dist-cjs/codegen/compile-gate.js +123 -0
  42. package/dist-cjs/codegen/index.d.ts +2 -0
  43. package/dist-cjs/codegen/index.js +14 -0
  44. package/dist-cjs/codegen/template.d.ts +18 -0
  45. package/dist-cjs/codegen/template.js +134 -0
  46. package/dist-cjs/errors.d.ts +37 -0
  47. package/dist-cjs/errors.js +3 -0
  48. package/dist-cjs/index.d.ts +11 -0
  49. package/dist-cjs/index.js +27 -0
  50. package/dist-cjs/ir/index.d.ts +1 -0
  51. package/dist-cjs/ir/index.js +3 -0
  52. package/dist-cjs/ir/types.d.ts +108 -0
  53. package/dist-cjs/ir/types.js +12 -0
  54. package/dist-cjs/mandate/index.d.ts +2 -0
  55. package/dist-cjs/mandate/index.js +6 -0
  56. package/dist-cjs/mandate/to-ir.d.ts +3 -0
  57. package/dist-cjs/mandate/to-ir.js +63 -0
  58. package/dist-cjs/mandate/types.d.ts +20 -0
  59. package/dist-cjs/mandate/types.js +9 -0
  60. package/dist-cjs/package.json +3 -0
  61. package/dist-cjs/predicate/encode.d.ts +10 -0
  62. package/dist-cjs/predicate/encode.js +252 -0
  63. package/dist-cjs/predicate/index.d.ts +1 -0
  64. package/dist-cjs/predicate/index.js +6 -0
  65. package/dist-cjs/record/decode.d.ts +79 -0
  66. package/dist-cjs/record/decode.js +441 -0
  67. package/dist-cjs/record/freshness.d.ts +30 -0
  68. package/dist-cjs/record/freshness.js +85 -0
  69. package/dist-cjs/record/index.d.ts +32 -0
  70. package/dist-cjs/record/index.js +191 -0
  71. package/dist-cjs/record/movements.d.ts +32 -0
  72. package/dist-cjs/record/movements.js +227 -0
  73. package/dist-cjs/record/rpc.d.ts +22 -0
  74. package/dist-cjs/record/rpc.js +74 -0
  75. package/dist-cjs/record/validate.d.ts +22 -0
  76. package/dist-cjs/record/validate.js +63 -0
  77. package/dist-cjs/registry/identify.d.ts +11 -0
  78. package/dist-cjs/registry/identify.js +87 -0
  79. package/dist-cjs/registry/index.d.ts +3 -0
  80. package/dist-cjs/registry/index.js +15 -0
  81. package/dist-cjs/registry/known-addresses.d.ts +16 -0
  82. package/dist-cjs/registry/known-addresses.js +53 -0
  83. package/dist-cjs/registry/protocols.d.ts +38 -0
  84. package/dist-cjs/registry/protocols.js +153 -0
  85. package/dist-cjs/review-card/builder.d.ts +14 -0
  86. package/dist-cjs/review-card/builder.js +264 -0
  87. package/dist-cjs/review-card/conflict.d.ts +40 -0
  88. package/dist-cjs/review-card/conflict.js +114 -0
  89. package/dist-cjs/review-card/cross-check.d.ts +11 -0
  90. package/dist-cjs/review-card/cross-check.js +151 -0
  91. package/dist-cjs/review-card/index.d.ts +3 -0
  92. package/dist-cjs/review-card/index.js +10 -0
  93. package/dist-cjs/run/index.d.ts +43 -0
  94. package/dist-cjs/run/index.js +237 -0
  95. package/dist-cjs/run/schemas.d.ts +2016 -0
  96. package/dist-cjs/run/schemas.js +226 -0
  97. package/dist-cjs/seams/index.d.ts +1 -0
  98. package/dist-cjs/seams/index.js +3 -0
  99. package/dist-cjs/seams/types.d.ts +66 -0
  100. package/dist-cjs/seams/types.js +12 -0
  101. package/dist-cjs/synth/address.d.ts +7 -0
  102. package/dist-cjs/synth/address.js +15 -0
  103. package/dist-cjs/synth/compose-from-recording.d.ts +65 -0
  104. package/dist-cjs/synth/compose-from-recording.js +366 -0
  105. package/dist-cjs/synth/deny-cases.d.ts +22 -0
  106. package/dist-cjs/synth/deny-cases.js +438 -0
  107. package/dist-cjs/synth/evaluate.d.ts +39 -0
  108. package/dist-cjs/synth/evaluate.js +428 -0
  109. package/dist-cjs/synth/harness.d.ts +16 -0
  110. package/dist-cjs/synth/harness.js +29 -0
  111. package/dist-cjs/synth/index.d.ts +10 -0
  112. package/dist-cjs/synth/index.js +28 -0
  113. package/dist-cjs/synth/lower.d.ts +23 -0
  114. package/dist-cjs/synth/lower.js +119 -0
  115. package/dist-cjs/synth/minimize.d.ts +4 -0
  116. package/dist-cjs/synth/minimize.js +41 -0
  117. package/dist-cjs/synth/predicate-literals.d.ts +5 -0
  118. package/dist-cjs/synth/predicate-literals.js +28 -0
  119. package/dist-cjs/synth/scope.d.ts +26 -0
  120. package/dist-cjs/synth/scope.js +81 -0
  121. package/dist-cjs/synth/synthesize-from-mandate.d.ts +5 -0
  122. package/dist-cjs/synth/synthesize-from-mandate.js +37 -0
  123. package/dist-cjs/synth/synthesize-from-recording.d.ts +49 -0
  124. package/dist-cjs/synth/synthesize-from-recording.js +759 -0
  125. package/dist-cjs/types.d.ts +311 -0
  126. package/dist-cjs/types.js +55 -0
  127. package/dist-cjs/verify/envelope.d.ts +15 -0
  128. package/dist-cjs/verify/envelope.js +23 -0
  129. package/dist-cjs/verify/index.d.ts +3 -0
  130. package/dist-cjs/verify/index.js +8 -0
  131. package/dist-cjs/verify/simulate.d.ts +31 -0
  132. package/dist-cjs/verify/simulate.js +261 -0
  133. package/dist-cjs/verify/verify.d.ts +21 -0
  134. package/dist-cjs/verify/verify.js +192 -0
  135. package/package.json +43 -3
  136. package/src/codegen/template.ts +3 -3
  137. package/src/contracts/policy-template/OZ_POLICY_TRAIT.md +171 -0
  138. package/src/record/corpus-fixtures.json +532 -0
  139. package/src/record/decode.ts +63 -10
  140. package/src/record/freshness.ts +34 -2
  141. package/src/record/index.ts +40 -0
  142. package/src/record/movements.ts +40 -7
  143. package/src/run/index.ts +277 -0
  144. package/src/run/schemas.ts +250 -0
  145. package/src/synth/address.ts +14 -0
  146. package/src/synth/compose-from-recording.ts +20 -9
  147. package/src/synth/deny-cases.ts +87 -4
  148. package/src/synth/evaluate.ts +2 -2
  149. package/src/synth/index.ts +4 -0
  150. package/src/synth/minimize.ts +7 -3
  151. package/src/synth/synthesize-from-recording.ts +200 -68
  152. package/src/types.ts +37 -1
  153. package/src/verify/simulate.ts +21 -2
  154. package/src/verify/verify.ts +21 -2
@@ -0,0 +1,40 @@
1
+ /** Minimal rule reference the classifier needs to compute the conflict kind.
2
+ * Carries the rule id (used in the annotation), the scope (contract +
3
+ * method), and an optional spending cap. The cap captures both the
4
+ * amount and the window so the classifier can compare apples to apples. */
5
+ export interface RuleRef {
6
+ id: string;
7
+ scope: {
8
+ contract?: string;
9
+ method?: string;
10
+ };
11
+ /** When set, the rule enforces this per-window spend cap (token +
12
+ * decimal-string amount + seconds window). */
13
+ spendingLimit?: {
14
+ token: string;
15
+ amount: string;
16
+ windowSeconds: number;
17
+ };
18
+ }
19
+ /** Structured annotation a single new-vs-existing rule comparison yields.
20
+ * Four mutually exclusive kinds; the union is exhaustive (the classifier
21
+ * never throws). */
22
+ export type ConflictAnnotation = {
23
+ kind: 'subsume';
24
+ existingRuleId: string;
25
+ detail: string;
26
+ } | {
27
+ kind: 'disjoint';
28
+ } | {
29
+ kind: 'counter_permissive';
30
+ existingRuleId: string;
31
+ newLimit: string;
32
+ existingLimit: string;
33
+ } | {
34
+ kind: 'window_divergent';
35
+ existingRuleId: string;
36
+ effectiveUnion: string;
37
+ };
38
+ /** Classify the relationship between a new rule and one existing rule.
39
+ * Pure: same pair -> same annotation. */
40
+ export declare function classifyConflict(newRule: RuleRef, existingRule: RuleRef): ConflictAnnotation;
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ // src/review-card/conflict.ts - the ConflictAnnotation 4-kind enum + the
3
+ // `classifyConflict` classifier.
4
+ //
5
+ // When a user installs a new policy alongside an existing rule set, the
6
+ // review card surfaces the relationship between the new rule and each
7
+ // existing rule as a structured annotation. A boolean (conflict / no
8
+ // conflict) is wrong: four qualitatively different shapes are possible, and
9
+ // each one tells the user something different about what would happen if
10
+ // both rules installed together.
11
+ //
12
+ // - subsume new rule's scope is a SUPERSET of an existing
13
+ // rule's. The new rule is broader; installing it on
14
+ // top of the existing rule weakens the existing
15
+ // restriction. The existing rule becomes a no-op.
16
+ // - disjoint new rule and existing rule are unrelated: their
17
+ // scopes do not overlap. Safe to install both.
18
+ // - counter_permissive new rule and existing rule allow CONFLICTING
19
+ // things at the same scope (one caps lower than the
20
+ // other on the same axis). The stricter one wins
21
+ // at evaluate; the looser is dead weight.
22
+ // - window_divergent new rule and existing rule have the same scope
23
+ // and the same per-window limit but DIFFERENT
24
+ // rolling windows. Effective union = sum (the user
25
+ // can spend `limit` per each window, independently).
26
+ //
27
+ // The classifier consumes a minimal `RuleRef` shape: the rule id, scope
28
+ // (contract + method), and an optional spending limit + window. Shapes
29
+ // without a limit fall back to scope-only classification.
30
+ Object.defineProperty(exports, "__esModule", { value: true });
31
+ exports.classifyConflict = classifyConflict;
32
+ /** Classify the relationship between a new rule and one existing rule.
33
+ * Pure: same pair -> same annotation. */
34
+ function classifyConflict(newRule, existingRule) {
35
+ // --- step 1: subsume? (new.scope ⊋ existing.scope) ---
36
+ //
37
+ // Subsume requires existing to be fully scoped (both contract AND method
38
+ // pinned) AND new to be at least partially broader than existing. A new
39
+ // rule with identical scope to existing is NOT a superset - it is the
40
+ // same rule and falls through to the same-scope branch below. If existing
41
+ // is "any contract" / "any method" then it is the wildest rule in the
42
+ // rule set; a new rule covering it is just a duplicate, not a subsume -
43
+ // fall through to disjoint.
44
+ const existingHasContract = existingRule.scope.contract !== undefined;
45
+ const existingHasMethod = existingRule.scope.method !== undefined;
46
+ const newIsBroaderOnContract = newRule.scope.contract === undefined && existingHasContract;
47
+ const newIsBroaderOnMethod = newRule.scope.method === undefined && existingHasMethod;
48
+ const newMatchesOnContract = newRule.scope.contract === undefined ||
49
+ (existingHasContract && newRule.scope.contract === existingRule.scope.contract);
50
+ const newMatchesOnMethod = newRule.scope.method === undefined ||
51
+ (existingHasMethod && newRule.scope.method === existingRule.scope.method);
52
+ if (existingHasContract &&
53
+ existingHasMethod &&
54
+ newMatchesOnContract &&
55
+ newMatchesOnMethod &&
56
+ (newIsBroaderOnContract || newIsBroaderOnMethod)) {
57
+ return {
58
+ kind: 'subsume',
59
+ existingRuleId: existingRule.id,
60
+ detail: `new rule scope (${describeScope(newRule)}) is a superset of existing rule (${describeScope(existingRule)})`,
61
+ };
62
+ }
63
+ // --- step 2: disjoint by scope? ---
64
+ if (existingHasContract && newRule.scope.contract !== undefined) {
65
+ if (newRule.scope.contract !== existingRule.scope.contract) {
66
+ return { kind: 'disjoint' };
67
+ }
68
+ }
69
+ if (existingHasMethod &&
70
+ newRule.scope.method !== undefined &&
71
+ newRule.scope.method !== existingRule.scope.method) {
72
+ return { kind: 'disjoint' };
73
+ }
74
+ // --- step 3: same scope -> compare spend caps (when both sides carry them) ---
75
+ if (newRule.spendingLimit && existingRule.spendingLimit) {
76
+ const a = newRule.spendingLimit;
77
+ const b = existingRule.spendingLimit;
78
+ if (a.token !== b.token) {
79
+ // Same scope, different tokens: each binds a different axis. They
80
+ // do not counter each other; treat as disjoint.
81
+ return { kind: 'disjoint' };
82
+ }
83
+ if (a.windowSeconds === b.windowSeconds) {
84
+ // Same scope, same token, same window: counter-permissive when the
85
+ // amounts disagree (the stricter wins at evaluate; the looser is
86
+ // dead weight). Equal amounts -> duplicate rule, treat as disjoint.
87
+ if (a.amount === b.amount) {
88
+ return { kind: 'disjoint' };
89
+ }
90
+ return {
91
+ kind: 'counter_permissive',
92
+ existingRuleId: existingRule.id,
93
+ newLimit: `${a.amount} ${a.token} / ${a.windowSeconds}s`,
94
+ existingLimit: `${b.amount} ${b.token} / ${b.windowSeconds}s`,
95
+ };
96
+ }
97
+ // Same scope + token, different windows: effective union is the sum
98
+ // of the two per-window caps (each window ticks independently).
99
+ const union = (BigInt(a.amount) + BigInt(b.amount)).toString();
100
+ return {
101
+ kind: 'window_divergent',
102
+ existingRuleId: existingRule.id,
103
+ effectiveUnion: `${union} ${a.token} per ${a.windowSeconds}s (new) + ${b.windowSeconds}s (existing)`,
104
+ };
105
+ }
106
+ // Same scope without comparable spend caps on both sides -> disjoint.
107
+ return { kind: 'disjoint' };
108
+ }
109
+ function describeScope(rule) {
110
+ const parts = [];
111
+ parts.push(rule.scope.contract ? `contract=${rule.scope.contract}` : 'contract=*');
112
+ parts.push(rule.scope.method ? `method=${rule.scope.method}` : 'method=*');
113
+ return parts.join(', ');
114
+ }
@@ -0,0 +1,11 @@
1
+ import type { PredicateNode } from '../types.ts';
2
+ import type { ReviewCardSummary } from './builder.ts';
3
+ /** Assert every leaf in the predicate appears as a constraint string in
4
+ * `summary.constraints`. Returns the missing templates (without the leaf
5
+ * values filled in) when any leaf was dropped. */
6
+ export declare function summaryCrossCheck(predicate: PredicateNode | null, summary: ReviewCardSummary): {
7
+ ok: true;
8
+ } | {
9
+ ok: false;
10
+ missingConstraints: string[];
11
+ };
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ // src/review-card/cross-check.ts - the non-hallucination guard.
3
+ //
4
+ // `summaryCrossCheck` walks EVERY leaf of the predicate and asserts the
5
+ // review-card summary quoted a constraint string for it. Returns
6
+ // `{ ok: false, missingConstraints }` if any leaf was dropped (or rendered
7
+ // into a template shape the cross-check does not recognise). A
8
+ // `PredicateNode = null` is always ok - the policy is OZ-only and the
9
+ // interpreter predicate did not exist.
10
+ //
11
+ // The guard is structural: every leaf the builder is supposed to render has
12
+ // exactly one template shape. The cross-check enumerates those shapes and
13
+ // demands the summary carries the corresponding string. A naive summary
14
+ // that drops a leaf (or fabricates a string not in the predicate) trips the
15
+ // guard.
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.summaryCrossCheck = summaryCrossCheck;
18
+ /** Assert every leaf in the predicate appears as a constraint string in
19
+ * `summary.constraints`. Returns the missing templates (without the leaf
20
+ * values filled in) when any leaf was dropped. */
21
+ function summaryCrossCheck(predicate, summary) {
22
+ if (predicate === null)
23
+ return { ok: true };
24
+ const expected = [];
25
+ collect(predicate, expected);
26
+ if (expected.length === 0)
27
+ return { ok: true };
28
+ const present = new Set(summary.constraints);
29
+ const missing = expected.filter((s) => !present.has(s));
30
+ if (missing.length === 0)
31
+ return { ok: true };
32
+ return { ok: false, missingConstraints: missing };
33
+ }
34
+ /** Walk the predicate and emit the EXACT constraint string the builder is
35
+ * expected to produce for each leaf, in walk order. Strings here MUST
36
+ * match the templates in `builder.ts` byte-for-byte; the cross-check is
37
+ * the structural claim that "every supported leaf shape is rendered". */
38
+ function collect(node, out) {
39
+ switch (node.op) {
40
+ case 'and':
41
+ case 'or':
42
+ for (const child of node.children)
43
+ collect(child, out);
44
+ return;
45
+ case 'not':
46
+ collect(node.child, out);
47
+ return;
48
+ case 'eq':
49
+ case 'lt':
50
+ case 'lte':
51
+ case 'gt':
52
+ case 'gte':
53
+ pushComparison(node.left, node.right, node.op, out);
54
+ return;
55
+ case 'in':
56
+ pushMembership(node.needle, node.haystack, out);
57
+ return;
58
+ }
59
+ }
60
+ function pushComparison(left, right, op, out) {
61
+ if (left.kind === 'call_contract' && op === 'eq' && right.kind === 'literal_address') {
62
+ out.push(`Contract must be ${right.value}`);
63
+ return;
64
+ }
65
+ if (left.kind === 'call_fn' && op === 'eq' && right.kind === 'literal_symbol') {
66
+ out.push(`Function must be ${right.value}`);
67
+ return;
68
+ }
69
+ if (left.kind === 'call_arg' && op === 'eq' && right.kind === 'literal_vec') {
70
+ out.push(`Path must be exactly [${right.elements.map(renderVecElement).join(', ')}]`);
71
+ return;
72
+ }
73
+ if (left.kind === 'invocation_count_in_window' && right.kind === 'literal_u32') {
74
+ out.push(`Invocations <= ${right.value} per ${left.windowSecs} seconds`);
75
+ return;
76
+ }
77
+ if (left.kind === 'amount' && right.kind === 'literal_i128') {
78
+ out.push(`Amount <= ${right.value}`);
79
+ return;
80
+ }
81
+ if (left.kind === 'oracle_price' && right.kind === 'literal_i128') {
82
+ out.push(`Only when oracle_price(${left.asset}) ${comparisonOpText(op)} ${right.value}`);
83
+ return;
84
+ }
85
+ }
86
+ function pushMembership(needle, haystack, out) {
87
+ if (needle.kind !== 'call_arg')
88
+ return;
89
+ const list = haystack.map(renderHaystackElement).join(', ');
90
+ out.push(`Recipient/arg must be one of [${list}]`);
91
+ }
92
+ function renderVecElement(leaf) {
93
+ switch (leaf.kind) {
94
+ case 'literal_address':
95
+ return leaf.value;
96
+ case 'literal_i128':
97
+ return leaf.value;
98
+ case 'literal_symbol':
99
+ return leaf.value;
100
+ case 'literal_u32':
101
+ return String(leaf.value);
102
+ case 'literal_u64':
103
+ return leaf.value;
104
+ case 'literal_bytes':
105
+ return leaf.value;
106
+ case 'literal_vec':
107
+ return `[${leaf.elements.map(renderVecElement).join(', ')}]`;
108
+ case 'call_contract':
109
+ case 'call_fn':
110
+ case 'call_arg':
111
+ case 'amount':
112
+ case 'window_spent':
113
+ case 'now':
114
+ case 'valid_until':
115
+ case 'invocation_count_in_window':
116
+ case 'oracle_price':
117
+ return `<${leaf.kind}>`;
118
+ }
119
+ }
120
+ function renderHaystackElement(leaf) {
121
+ if (leaf.kind === 'literal_address')
122
+ return leaf.value;
123
+ if (leaf.kind === 'literal_i128')
124
+ return leaf.value;
125
+ if (leaf.kind === 'literal_symbol')
126
+ return leaf.value;
127
+ if (leaf.kind === 'literal_u32')
128
+ return String(leaf.value);
129
+ if (leaf.kind === 'literal_u64')
130
+ return leaf.value;
131
+ if (leaf.kind === 'literal_bytes')
132
+ return leaf.value;
133
+ if (leaf.kind === 'literal_vec') {
134
+ return `[${leaf.elements.map(renderHaystackElement).join(', ')}]`;
135
+ }
136
+ return `<${leaf.kind}>`;
137
+ }
138
+ function comparisonOpText(op) {
139
+ switch (op) {
140
+ case 'lt':
141
+ return '<';
142
+ case 'lte':
143
+ return '<=';
144
+ case 'gt':
145
+ return '>';
146
+ case 'gte':
147
+ return '>=';
148
+ case 'eq':
149
+ return '==';
150
+ }
151
+ }
@@ -0,0 +1,3 @@
1
+ export { buildReviewCardSummary, type ReviewCardSummary, } from './builder.ts';
2
+ export { type ConflictAnnotation, classifyConflict, type RuleRef, } from './conflict.ts';
3
+ export { summaryCrossCheck } from './cross-check.ts';
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ // src/review-card/index.ts - re-export the deterministic review-card surface.
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.summaryCrossCheck = exports.classifyConflict = exports.buildReviewCardSummary = void 0;
5
+ var builder_ts_1 = require("./builder.js");
6
+ Object.defineProperty(exports, "buildReviewCardSummary", { enumerable: true, get: function () { return builder_ts_1.buildReviewCardSummary; } });
7
+ var conflict_ts_1 = require("./conflict.js");
8
+ Object.defineProperty(exports, "classifyConflict", { enumerable: true, get: function () { return conflict_ts_1.classifyConflict; } });
9
+ var cross_check_ts_1 = require("./cross-check.js");
10
+ Object.defineProperty(exports, "summaryCrossCheck", { enumerable: true, get: function () { return cross_check_ts_1.summaryCrossCheck; } });
@@ -0,0 +1,43 @@
1
+ import { type ErrorCode, type ProposedPolicy, type RecordedTransaction, type ToolError, type ToolResponse } from '../index.ts';
2
+ import { type RecordTransactionInput, type SynthesizePolicyInput } from './schemas.ts';
3
+ export type { RecordTransactionInput, SynthesizePolicyInput } from './schemas.ts';
4
+ export { ComposeUserResponsesSchema, InterpreterOptionsSchema, MandateSpecSchema, NetworkSchema, OzAdapterConfigSchema, RecordedTransactionSchema, RecordTransactionInputSchema, SynthesizePolicyInputSchema, ToolErrorSchema, } from './schemas.ts';
5
+ export type RunRecordTransactionInput = RecordTransactionInput;
6
+ export type RunSynthesizePolicyInput = SynthesizePolicyInput;
7
+ /** `record_transaction` body - wraps `recordTransaction`. The tool input
8
+ * matches the core RecordInput minus the injected `fetcher` (the transport
9
+ * layer does not own the RPC). Returns the core ToolResponse unchanged.
10
+ *
11
+ * Item 6: a try/catch envelope wraps the core call. The core's own
12
+ * `recordTransaction` already returns `{ok:false, error}` for Zod + decoder
13
+ * failures, but a throw from the SDK (e.g. a malformed hash handed to
14
+ * `@stellar/stellar-sdk`'s StrKey decoder) would otherwise surface as
15
+ * "[object Object]" in the MCP transport. The envelope converts any
16
+ * non-ToolResponse throw into a structured `RECORDING_FAILED` ToolError so
17
+ * the agent always sees a machine-readable error code. */
18
+ export declare function runRecordTransaction(raw: unknown): Promise<ToolResponse<RecordedTransaction>>;
19
+ /** `synthesize_policy` body - discriminated union on `source`:
20
+ * - `mandate` -> synthesizeFromMandate
21
+ * - `recording` -> synthesizeFromRecording
22
+ * Exposing BOTH front-ends through ONE tool keeps the MCP surface tiny while
23
+ * letting the agent pick the deterministic or the inferred path. The CLI
24
+ * routes the same way.
25
+ *
26
+ * Item 6: same try/catch envelope as `runRecordTransaction`. The core
27
+ * `synthesizeFromRecording` already converts ToolError-shaped throws to
28
+ * `{ok:false, error}`, but a raw throw from the SDK (e.g. an unexpected
29
+ * XDR decode error in the adapter) would otherwise surface as
30
+ * "[object Object]" in the MCP transport. */
31
+ export declare function runSynthesizePolicy(raw: unknown): Promise<ToolResponse<ProposedPolicy>>;
32
+ /** Build a canonical ToolError for a thrown exception caught by the tool
33
+ * envelope. The MCP SDK stringifies thrown objects as "[object Object]" by
34
+ * default, so we extract a string-friendly message and tag the original
35
+ * error in `details` for the agent to inspect. The `code` is the tool's
36
+ * domain code (RECORDING_FAILED for `record_transaction`, SYNTHESIS_ERROR
37
+ * for `synthesize_policy`) so the agent dispatches on the same code the
38
+ * structured ToolError would carry.
39
+ *
40
+ * Exported as `_caughtError` (the leading underscore signals the test-only
41
+ * seam) so the suite in run/index.test.ts can drive the envelope path
42
+ * without standing up a full recordTransaction pipeline. */
43
+ export declare function caughtError(toolName: 'record_transaction' | 'synthesize_policy', code: ErrorCode, e: unknown): ToolError;
@@ -0,0 +1,237 @@
1
+ "use strict";
2
+ // packages/policy-synth/src/run/index.ts
3
+ //
4
+ // Tool-body adapters for the two core front-ends. Reachable as
5
+ // `@crediolabs/policy-synth/run` from both the MCP server (which re-exports
6
+ // for tool-registration glue) and the CLI (which calls them directly). Each
7
+ // body is a THIN adapter over the pure core:
8
+ //
9
+ // 1. Re-validate the parsed input via Zod. This is a defence-in-depth check -
10
+ // the SDK has already parsed it through the registered schema, but we never
11
+ // want a tool body to throw on garbage (the SDK treats uncaught throws as
12
+ // transport errors and the agent loses the machine-readable ToolError).
13
+ // 2. Dispatch to the matching core entry point with the minimum required
14
+ // inputs.
15
+ // 3. Return the core's ToolResponse<T> unchanged. The server layer maps it
16
+ // to the MCP envelope; nothing here knows about MCP.
17
+ //
18
+ // No business logic. No retries. No session state. The same call shape can
19
+ // drive the CLI (which calls into the same core directly without MCP).
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.ToolErrorSchema = exports.SynthesizePolicyInputSchema = exports.RecordTransactionInputSchema = exports.RecordedTransactionSchema = exports.OzAdapterConfigSchema = exports.NetworkSchema = exports.MandateSpecSchema = exports.InterpreterOptionsSchema = exports.ComposeUserResponsesSchema = void 0;
22
+ exports.runRecordTransaction = runRecordTransaction;
23
+ exports.runSynthesizePolicy = runSynthesizePolicy;
24
+ exports.caughtError = caughtError;
25
+ const index_ts_1 = require("../index.js");
26
+ const schemas_ts_1 = require("./schemas.js");
27
+ // Re-export the underlying Zod schemas so the MCP package (and any other
28
+ // downstream consumer) can import the canonical input shapes from the same
29
+ // module that owns the tool-body glue. The strict schemas are the source of
30
+ // truth - MCP tool shapes are derived from them.
31
+ var schemas_ts_2 = require("./schemas.js");
32
+ Object.defineProperty(exports, "ComposeUserResponsesSchema", { enumerable: true, get: function () { return schemas_ts_2.ComposeUserResponsesSchema; } });
33
+ Object.defineProperty(exports, "InterpreterOptionsSchema", { enumerable: true, get: function () { return schemas_ts_2.InterpreterOptionsSchema; } });
34
+ Object.defineProperty(exports, "MandateSpecSchema", { enumerable: true, get: function () { return schemas_ts_2.MandateSpecSchema; } });
35
+ Object.defineProperty(exports, "NetworkSchema", { enumerable: true, get: function () { return schemas_ts_2.NetworkSchema; } });
36
+ Object.defineProperty(exports, "OzAdapterConfigSchema", { enumerable: true, get: function () { return schemas_ts_2.OzAdapterConfigSchema; } });
37
+ Object.defineProperty(exports, "RecordedTransactionSchema", { enumerable: true, get: function () { return schemas_ts_2.RecordedTransactionSchema; } });
38
+ Object.defineProperty(exports, "RecordTransactionInputSchema", { enumerable: true, get: function () { return schemas_ts_2.RecordTransactionInputSchema; } });
39
+ Object.defineProperty(exports, "SynthesizePolicyInputSchema", { enumerable: true, get: function () { return schemas_ts_2.SynthesizePolicyInputSchema; } });
40
+ Object.defineProperty(exports, "ToolErrorSchema", { enumerable: true, get: function () { return schemas_ts_2.ToolErrorSchema; } });
41
+ /** `record_transaction` body - wraps `recordTransaction`. The tool input
42
+ * matches the core RecordInput minus the injected `fetcher` (the transport
43
+ * layer does not own the RPC). Returns the core ToolResponse unchanged.
44
+ *
45
+ * Item 6: a try/catch envelope wraps the core call. The core's own
46
+ * `recordTransaction` already returns `{ok:false, error}` for Zod + decoder
47
+ * failures, but a throw from the SDK (e.g. a malformed hash handed to
48
+ * `@stellar/stellar-sdk`'s StrKey decoder) would otherwise surface as
49
+ * "[object Object]" in the MCP transport. The envelope converts any
50
+ * non-ToolResponse throw into a structured `RECORDING_FAILED` ToolError so
51
+ * the agent always sees a machine-readable error code. */
52
+ async function runRecordTransaction(raw) {
53
+ const parsed = schemas_ts_1.RecordTransactionInputSchema.safeParse(raw);
54
+ if (!parsed.success) {
55
+ return {
56
+ ok: false,
57
+ error: validationError('record_transaction', parsed.error.issues),
58
+ };
59
+ }
60
+ const input = parsed.data;
61
+ // Strip the wire-only `confidenceOverride` and pass the rest straight through.
62
+ const coreInput = {
63
+ network: input.network,
64
+ ...(input.hash !== undefined ? { hash: input.hash } : {}),
65
+ ...(input.xdr !== undefined ? { xdr: input.xdr } : {}),
66
+ ...(input.confidenceOverride !== undefined
67
+ ? { confidenceOverride: input.confidenceOverride }
68
+ : {}),
69
+ };
70
+ try {
71
+ return await (0, index_ts_1.recordTransaction)(coreInput);
72
+ }
73
+ catch (e) {
74
+ return {
75
+ ok: false,
76
+ error: caughtError('record_transaction', 'RECORDING_FAILED', e),
77
+ };
78
+ }
79
+ }
80
+ /** `synthesize_policy` body - discriminated union on `source`:
81
+ * - `mandate` -> synthesizeFromMandate
82
+ * - `recording` -> synthesizeFromRecording
83
+ * Exposing BOTH front-ends through ONE tool keeps the MCP surface tiny while
84
+ * letting the agent pick the deterministic or the inferred path. The CLI
85
+ * routes the same way.
86
+ *
87
+ * Item 6: same try/catch envelope as `runRecordTransaction`. The core
88
+ * `synthesizeFromRecording` already converts ToolError-shaped throws to
89
+ * `{ok:false, error}`, but a raw throw from the SDK (e.g. an unexpected
90
+ * XDR decode error in the adapter) would otherwise surface as
91
+ * "[object Object]" in the MCP transport. */
92
+ async function runSynthesizePolicy(raw) {
93
+ const parsed = schemas_ts_1.SynthesizePolicyInputSchema.safeParse(raw);
94
+ if (!parsed.success) {
95
+ return {
96
+ ok: false,
97
+ error: validationError('synthesize_policy', parsed.error.issues),
98
+ };
99
+ }
100
+ const input = parsed.data;
101
+ const ozConfig = resolveOzConfig(input);
102
+ try {
103
+ if (input.source === 'mandate') {
104
+ // Zod's optional fields widen to `T | undefined`, which the core's
105
+ // exact-optional MandateSpec rejects; the schema already validated the
106
+ // shape, so assert it (same pattern as the recordedTx cast below).
107
+ return await (0, index_ts_1.synthesizeFromMandate)(input.mandate, ozConfig);
108
+ }
109
+ // recording source
110
+ const recorded = input.recordedTx;
111
+ return await (0, index_ts_1.synthesizeFromRecording)(recorded, {
112
+ network: input.network,
113
+ ...(input.userResponses !== undefined ? { userResponses: input.userResponses } : {}),
114
+ ...(input.confidenceOverride !== undefined
115
+ ? { confidenceOverride: input.confidenceOverride }
116
+ : {}),
117
+ ...(input.interpreter !== undefined ? { interpreter: input.interpreter } : {}),
118
+ }, ozConfig);
119
+ }
120
+ catch (e) {
121
+ return {
122
+ ok: false,
123
+ error: caughtError('synthesize_policy', 'SYNTHESIS_ERROR', e),
124
+ };
125
+ }
126
+ }
127
+ function resolveOzConfig(input) {
128
+ if (input.ozConfig)
129
+ return input.ozConfig;
130
+ // The mandate path is network-agnostic; fall back to mainnet so the
131
+ // placeholder OZ instance addresses are deterministic.
132
+ return (0, index_ts_1.placeholderOzConfig)('mainnet');
133
+ }
134
+ /** Build a canonical ToolError for a Zod validation failure. The remediation
135
+ * hint points the agent back at the right tool with an empty arg bag - the
136
+ * tool name IS the machine-readable hint. */
137
+ function validationError(toolName, issues) {
138
+ const code = toolName === 'record_transaction' ? 'RECORDING_FAILED' : 'SYNTHESIS_ERROR';
139
+ return {
140
+ code,
141
+ message: `${toolName}: invalid input: ${issues
142
+ .map((i) => `${i.path.join('.') || '<root>'}: ${i.message}`)
143
+ .join('; ')}`,
144
+ severity: 'error',
145
+ retryable: false,
146
+ remediation: {
147
+ toolCall: { name: toolName, args: {} },
148
+ },
149
+ };
150
+ }
151
+ /** Build a canonical ToolError for a thrown exception caught by the tool
152
+ * envelope. The MCP SDK stringifies thrown objects as "[object Object]" by
153
+ * default, so we extract a string-friendly message and tag the original
154
+ * error in `details` for the agent to inspect. The `code` is the tool's
155
+ * domain code (RECORDING_FAILED for `record_transaction`, SYNTHESIS_ERROR
156
+ * for `synthesize_policy`) so the agent dispatches on the same code the
157
+ * structured ToolError would carry.
158
+ *
159
+ * Exported as `_caughtError` (the leading underscore signals the test-only
160
+ * seam) so the suite in run/index.test.ts can drive the envelope path
161
+ * without standing up a full recordTransaction pipeline. */
162
+ function caughtError(toolName, code, e) {
163
+ const message = describeThrown(e, toolName);
164
+ const details = { thrown: safeStringify(e) };
165
+ return {
166
+ code,
167
+ message: `${toolName}: unhandled throw escaped core envelope: ${message}`,
168
+ severity: 'error',
169
+ retryable: false,
170
+ remediation: {
171
+ toolCall: { name: toolName, args: {} },
172
+ },
173
+ details,
174
+ };
175
+ }
176
+ /** Build a human-readable message for an unknown caught value. Order matters:
177
+ * 1. `Error` instances use `.message` (or fallback to the class name).
178
+ * 2. Native strings pass through verbatim.
179
+ * 3. Objects with a string `message` field use that field (mirrors the
180
+ * shape thrown by some SDKs that package errors as plain objects).
181
+ * 4. Anything else falls back to a JSON-shaped summary, never to
182
+ * "[object Object]".
183
+ * The full payload is also captured in `details.thrown` via
184
+ * `safeStringify` so the agent can inspect the original value without
185
+ * risking an infinite loop on circular refs. */
186
+ function describeThrown(e, toolName) {
187
+ if (e instanceof Error) {
188
+ return e.message || `${toolName}: ${e.name || 'Error'}`;
189
+ }
190
+ if (typeof e === 'string') {
191
+ return e;
192
+ }
193
+ if (e !== null && typeof e === 'object') {
194
+ const obj = e;
195
+ const m = obj.message;
196
+ if (typeof m === 'string' && m.length > 0) {
197
+ return truncate(m);
198
+ }
199
+ return truncate(safeStringify(e));
200
+ }
201
+ return `${toolName}: caught non-Error throw of type ${typeof e}`;
202
+ }
203
+ /** Hard cap on the human-readable message embedded in the ToolError. The full
204
+ * payload is still preserved in `details.thrown` so the agent can inspect
205
+ * it - the truncation is only to keep the top-level message small enough to
206
+ * fit comfortably in transport logs. */
207
+ const MAX_MESSAGE_LEN = 512;
208
+ /** JSON.stringify that survives circular refs and very large payloads. The
209
+ * output is itself bounded by the same `MAX_DETAILS_LEN` so a thrown object
210
+ * with megabytes of buffer data cannot bloat the WHOLE envelope. */
211
+ const MAX_DETAILS_LEN = 4096;
212
+ function safeStringify(v) {
213
+ const seen = new WeakSet();
214
+ const json = JSON.stringify(v, (_k, value) => {
215
+ if (typeof value === 'bigint')
216
+ return value.toString();
217
+ if (typeof value === 'function')
218
+ return `[function ${value.name || 'anonymous'}]`;
219
+ if (value instanceof Error) {
220
+ return { name: value.name, message: value.message, stack: value.stack };
221
+ }
222
+ if (value !== null && typeof value === 'object') {
223
+ if (seen.has(value))
224
+ return '[Circular]';
225
+ seen.add(value);
226
+ }
227
+ return value;
228
+ }, 2);
229
+ if (json === undefined)
230
+ return '<unserializable>';
231
+ return truncate(json, MAX_DETAILS_LEN);
232
+ }
233
+ function truncate(s, cap = MAX_MESSAGE_LEN) {
234
+ if (s.length <= cap)
235
+ return s;
236
+ return `${s.slice(0, cap)}\n…[truncated ${s.length - cap} chars]`;
237
+ }