@crediolabs/policy-builder-cli 0.1.18 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -58,9 +58,11 @@ small: the two commands that fit a pipe.
58
58
 
59
59
  ## Security model
60
60
 
61
- The CLI holds no key material and signs nothing. The on-chain interpreter the
62
- policies target is **unaudited**; see the
63
- [architecture document](https://github.com/untangledfinance/octogate/blob/main/docs/architecture.md).
61
+ The CLI holds no key material and signs nothing. See the
62
+ [architecture document](https://github.com/untangledfinance/oz-policy-builder/blob/main/docs/architecture.md)
63
+ for what the on-chain interpreter does and does not enforce, and the
64
+ [repository README](https://github.com/untangledfinance/oz-policy-builder#readme) for
65
+ the contracts' audit status.
64
66
 
65
67
  ## License
66
68
 
@@ -4,16 +4,11 @@
4
4
  // Usage:
5
5
  // policy-builder record --network <mainnet|testnet> --hash <tx>
6
6
  // [--xdr <b64>] [--json] [--quiet] [--out <path>]
7
- // policy-builder synthesize --mandate <path.json>
8
- // [--oz-config <path.json>] [--confidence <0..1>]
9
- // [--explain] [--json] [--quiet] [--out <path>]
10
7
  // policy-builder synthesize --recorded-tx <path.json> --network <mainnet|testnet>
11
8
  // [--responses <path.json>]
12
- // [--oz-config <path.json>] [--confidence <0..1>]
9
+ // [--confidence <0..1>]
13
10
  // [--smart-account <C...>] [--install-nonce <n>]
14
- // [--oracle-max-staleness <s>] [--oracle-max-deviation <bps>]
15
- // [--window-seconds <n>] [--valid-until <ledger>]
16
- // [--limit-amount <i128str>] [--invocation-limit <n>]
11
+ // [--valid-until <ledger>] [--limit-amount <i128str>]
17
12
  // [--recipient <C...|G...>]...
18
13
  // [--explain] [--json] [--quiet] [--out <path>]
19
14
  //
@@ -69,52 +64,33 @@ function printHelp() {
69
64
  Usage:
70
65
  policy-builder record --network <mainnet|testnet> --hash <tx> | --xdr <b64>
71
66
  [--json] [--quiet] [--out <path>]
72
- policy-builder synthesize --mandate <path.json>
73
- [--oz-config <path.json>] [--confidence <0..1>]
74
- [--json] [--quiet] [--out <path>]
75
67
  policy-builder synthesize --recorded-tx <path.json> --network <mainnet|testnet>
76
68
  [--responses <path.json>]
77
- [--oz-config <path.json>] [--confidence <0..1>]
69
+ [--confidence <0..1>]
78
70
  [--smart-account <C...>] [--install-nonce <n>]
79
- [--oracle-max-staleness <s>] [--oracle-max-deviation <bps>]
80
- [--window-seconds <n>] [--valid-until <ledger>]
81
- [--limit-amount <i128str>] [--invocation-limit <n>]
71
+ [--valid-until <ledger>] [--limit-amount <i128str>]
82
72
  [--recipient <C...|G...>]...
83
- [--json] [--quiet] [--out <path>]
73
+ [--explain] [--json] [--quiet] [--out <path>]
84
74
 
85
75
  Flags:
86
76
  --json emit machine-readable JSON on stdout
87
77
  --quiet suppress progress / non-error output
88
78
  --out write artefact to file (JSON)
89
- --oz-config path to a JSON OzAdapterConfig (custom OZ instance
90
- addresses for the network); applies to both the
91
- --mandate and --recorded-tx synthesize paths
92
79
  --confidence recorder confidence GATE threshold (0..1, inclusive);
93
80
  rejects when parseConfidence.overall < threshold; does
94
81
  not change the recording's parseConfidence.thresholdUsed
95
82
  in the output
96
- --smart-account C... account to opt into the interpreter adapter, so
97
- non-OZ constraints (per-method scoping, invocation-count
98
- windows, oracle bounds, exact hop paths) lower to a real
99
- predicate document instead of warnings
83
+ --smart-account C... account the interpreter policy installs against;
84
+ required for the recording to lower to a predicate
85
+ document (amount caps, recipient allowlists, exact
86
+ hop paths) instead of warnings
100
87
  --install-nonce per-rule install nonce for the interpreter policy
101
88
  (default 1); requires --smart-account
102
- --oracle-max-staleness interpreter oracle max staleness in seconds
103
- (positive integer <= 600, tighten-only);
104
- requires --smart-account
105
- --oracle-max-deviation interpreter oracle max deviation in basis points
106
- (positive integer <= 200, tighten-only);
107
- requires --smart-account
108
- --window-seconds per-field override of userResponses.windowSeconds;
109
- overrides the same field from --responses (CLI flag
110
- wins)
111
89
  --valid-until per-field override of userResponses.validUntilLedger;
112
90
  overrides the same field from --responses
113
91
  --limit-amount per-field override of userResponses.limitAmount
114
92
  (i128 decimal string); overrides the same field
115
93
  from --responses
116
- --invocation-limit per-field override of userResponses.invocationLimit;
117
- overrides the same field from --responses
118
94
  --recipient swap-recipient allowlist entry (C... contract or
119
95
  G... wallet) for a SoroSwap swap; REPEATABLE. Absent,
120
96
  the recorded recipient is pinned by default; supplying
@@ -1,22 +1,20 @@
1
1
  // packages/policy-builder-cli/src/commands/synthesize.ts
2
2
  //
3
3
  // `policy-builder synthesize` subcommand. Dispatches to ONE of the two
4
- // front-ends (mandate / recording) based on which file flag is supplied:
5
- // --mandate <path.json> -> synthesizeFromMandate
4
+ // One front-end:
6
5
  // --recorded-tx <path.json> -> synthesizeFromRecording
7
6
  //
8
7
  // The CLI mirrors the MCP tool's discriminated union: one subcommand, two
9
8
  // front-ends, mutually exclusive.
10
9
  //
11
- // Per-field response flags (--window-seconds, --valid-until, --limit-amount,
12
- // --invocation-limit) merge into `userResponses`. A flag overrides the same
10
+ // Per-field response flags (--valid-until, --limit-amount) merge into
11
+ // `userResponses`. A flag overrides the same
13
12
  // field from --responses (CLI flags are explicit; the file is a default bag).
14
- // Oracle params (--oracle-max-staleness, --oracle-max-deviation) are part of
15
13
  // the interpreter opt-in and are rejected without --smart-account; tighten-only
16
14
  // bounds are validated by the core.
17
15
  //
18
16
  // --explain (Phase 1) makes the synthesised policy human-readable. The
19
- // orchestrator attaches the in-memory predicate tree + a SimulationResult
17
+ // orchestrator attaches the in-memory predicate tree
20
18
  // to the success envelope; the CLI builds the deterministic review card
21
19
  // from those inputs and emits it alongside the policy. Without --explain
22
20
  // the output is byte-identical to today.
@@ -29,12 +27,10 @@ import { CliError, formatToolResponse, parsePairs, readJsonFile } from "../outpu
29
27
  const POSITIVE_INT_RE = /^[0-9]+$/;
30
28
  export async function runSynthesizeCommand(argv, flags) {
31
29
  const pairs = parsePairs(argv);
32
- const hasMandate = Boolean(pairs.mandate);
33
- const hasRecorded = Boolean(pairs['recorded-tx']);
34
- if (hasMandate === hasRecorded) {
30
+ if (!pairs['recorded-tx']) {
35
31
  throw new CliError({
36
32
  code: 'CLI_MISSING_ARG',
37
- message: 'synthesize: exactly one of --mandate <path> or --recorded-tx <path> is required',
33
+ message: 'synthesize: --recorded-tx <path> is required',
38
34
  severity: 'error',
39
35
  retryable: false,
40
36
  });
@@ -45,16 +41,6 @@ export async function runSynthesizeCommand(argv, flags) {
45
41
  // output gains `review` + `predicateTree` and remains unchanged
46
42
  // otherwise.
47
43
  const explain = pairs.explain !== undefined;
48
- if (hasMandate) {
49
- const mandate = readJsonFile(pairs.mandate);
50
- const args = { source: 'mandate', mandate };
51
- applySharedFlags(args, pairs, explain);
52
- const res = await runSynthesizePolicy(args);
53
- if (explain)
54
- emitExplainBlock(res, flags);
55
- return formatToolResponse(res, flags, 'synthesize(mandate)');
56
- }
57
- // hasRecorded
58
44
  const recordedFile = readJsonFile(pairs['recorded-tx']);
59
45
  // Accept either a bare RecordedTransaction or the `{ ok, data }` artifact that
60
46
  // `record --out` writes (same shape as `--json`), so `record --out X` followed
@@ -90,10 +76,8 @@ export async function runSynthesizeCommand(argv, flags) {
90
76
  // Per-field overrides. Each entry: argv flag name, userResponses key,
91
77
  // and a parser that validates the raw string.
92
78
  const userResponseFlags = [
93
- ['window-seconds', 'windowSeconds', parsePositiveInt],
94
79
  ['valid-until', 'validUntilLedger', parsePositiveInt],
95
80
  ['limit-amount', 'limitAmount', parseI128String],
96
- ['invocation-limit', 'invocationLimit', parsePositiveInt],
97
81
  ];
98
82
  for (const [flag, key, parse] of userResponseFlags) {
99
83
  if (pairs[flag] !== undefined) {
@@ -122,9 +106,10 @@ export async function runSynthesizeCommand(argv, flags) {
122
106
  if (Object.keys(userResponses).length > 0)
123
107
  args.userResponses = userResponses;
124
108
  applySharedFlags(args, pairs, explain);
125
- // --smart-account <C...> opts into the interpreter adapter, so constraints OZ
126
- // cannot express (per-method scoping, invocation-count windows, oracle bounds,
127
- // exact hop paths) lower to a real predicate document instead of just warnings.
109
+ // --smart-account <C...> names the account the interpreter policy installs
110
+ // against. It is what lets the recording lower to a real predicate document
111
+ // (amount caps, recipient allowlists, exact hop paths) rather than warnings;
112
+ // without it the core reports that no installable policy was synthesised.
128
113
  // The core validates the address and installNonce; a bad value surfaces there.
129
114
  //
130
115
  // Use `!== undefined` (not truthy) so `--smart-account ""` and `--install-nonce`
@@ -134,9 +119,6 @@ export async function runSynthesizeCommand(argv, flags) {
134
119
  // when it had been silently skipped.
135
120
  const smartAccountRaw = pairs['smart-account'];
136
121
  const installNonceRaw = pairs['install-nonce'];
137
- const oracleStalenessRaw = pairs['oracle-max-staleness'];
138
- const oracleDeviationRaw = pairs['oracle-max-deviation'];
139
- // --install-nonce and the oracle knobs are interpreter-only. Reject up
140
122
  // front so they cannot be silently dropped when --smart-account is absent.
141
123
  if (smartAccountRaw === undefined) {
142
124
  if (installNonceRaw !== undefined) {
@@ -147,14 +129,6 @@ export async function runSynthesizeCommand(argv, flags) {
147
129
  retryable: false,
148
130
  });
149
131
  }
150
- if (oracleStalenessRaw !== undefined || oracleDeviationRaw !== undefined) {
151
- throw new CliError({
152
- code: 'CLI_MISSING_ARG',
153
- message: 'synthesize: --oracle-max-staleness / --oracle-max-deviation require --smart-account <C...> (interpreter opt-in)',
154
- severity: 'error',
155
- retryable: false,
156
- });
157
- }
158
132
  }
159
133
  else {
160
134
  const smartAccount = smartAccountRaw.trim();
@@ -187,19 +161,8 @@ export async function runSynthesizeCommand(argv, flags) {
187
161
  }
188
162
  interpreter.installNonce = nonce;
189
163
  }
190
- // Oracle params only attach when at least one bound was provided. The
191
164
  // core validates tighten-only (maxStalenessSeconds <= 600,
192
165
  // maxDeviationBps <= 200) - a too-loose value surfaces as SYNTHESIS_ERROR.
193
- if (oracleStalenessRaw !== undefined || oracleDeviationRaw !== undefined) {
194
- const oracleParams = {};
195
- if (oracleStalenessRaw !== undefined) {
196
- oracleParams.maxStalenessSeconds = parsePositiveInt(oracleStalenessRaw, '--oracle-max-staleness');
197
- }
198
- if (oracleDeviationRaw !== undefined) {
199
- oracleParams.maxDeviationBps = parsePositiveInt(oracleDeviationRaw, '--oracle-max-deviation');
200
- }
201
- interpreter.oracleParams = oracleParams;
202
- }
203
166
  args.interpreter = interpreter;
204
167
  }
205
168
  if (explain)
@@ -213,13 +176,9 @@ export async function runSynthesizeCommand(argv, flags) {
213
176
  emitExplainBlock(res, flags);
214
177
  return formatToolResponse(res, flags, 'synthesize(recording)');
215
178
  }
216
- /** Apply flags shared between the mandate and recording synthesize paths:
217
- * --oz-config, --confidence, --explain. Pulled out so adding a shared flag
218
- * adds one branch here rather than two copies in the command body. */
179
+ /** Apply the flags that are not part of the recording payload itself:
180
+ * --confidence, --explain. */
219
181
  function applySharedFlags(args, pairs, explain) {
220
- if (pairs['oz-config'] !== undefined) {
221
- args.ozConfig = readOzConfigFile(pairs['oz-config']);
222
- }
223
182
  if (pairs.confidence !== undefined) {
224
183
  args.confidenceOverride = { threshold: parseConfidence(pairs.confidence) };
225
184
  }
@@ -239,15 +198,15 @@ function emitExplainBlock(res, flags) {
239
198
  const review = buildReviewCardSummary(
240
199
  // The orchestrator's `predicateTree` is the exact in-memory AST
241
200
  // (canonical JSON shape). The builder's input is typed as
242
- // `PredicateNode | null`; the orchestrator's `null` is the truthful
243
- // OZ-only / mandate value, so a null here is honest.
244
- (res.explain.predicateTree ?? null), res.data?.policyRefs ?? [], res.data?.contextRule ?? {
201
+ // `PredicateNode | null`; a null is the truthful value when the
202
+ // interpreter adapter was not engaged.
203
+ (res.explain.predicateTree ?? null), res.data?.contextRule ?? {
245
204
  contextRuleType: { kind: 'default' },
246
205
  name: 'unknown',
247
206
  validUntilLedger: null,
248
207
  signers: [],
249
208
  policies: [],
250
- }, res.explain.simulation);
209
+ }, 'interpreter-v1');
251
210
  res.data.review =
252
211
  review;
253
212
  res.data.predicateTree = res.explain.predicateTree;
@@ -269,23 +228,6 @@ function emitExplainBlock(res, flags) {
269
228
  }
270
229
  }
271
230
  }
272
- /** Read and validate an OzAdapterConfig JSON file. Throws CLI_FILE_NOT_FOUND /
273
- * CLI_INVALID_JSON for filesystem / parse failures; the core's strict schema
274
- * on `ozConfig` catches shape mismatches downstream. */
275
- function readOzConfigFile(path) {
276
- const value = readJsonFile(path);
277
- if (value === null || typeof value !== 'object' || Array.isArray(value)) {
278
- throw new CliError({
279
- code: 'CLI_INVALID_JSON',
280
- message: `synthesize: --oz-config ${path} must be a JSON object`,
281
- severity: 'error',
282
- retryable: false,
283
- });
284
- }
285
- return value;
286
- }
287
- /** Parse and validate `--confidence <n>` as a finite number in [0, 1]. A
288
- * threshold above 1 would disable the recorder gate; reject it up front. */
289
231
  function parseConfidence(raw) {
290
232
  const n = Number(raw);
291
233
  if (!Number.isFinite(n) || n < 0 || n > 1) {
@@ -298,8 +240,7 @@ function parseConfidence(raw) {
298
240
  }
299
241
  return n;
300
242
  }
301
- /** Parse a strictly positive integer (windowSeconds, validUntilLedger,
302
- * invocationLimit, oracleParams bounds). The core re-validates these with
243
+ /** Parse a strictly positive integer (validUntilLedger,
303
244
  * field-specific caps; the CLI just enforces "looks like an integer > 0".
304
245
  * The `^[0-9]+$` regex already pins the shape to a non-negative integer, so
305
246
  * the only thing left to check is "not zero". */
@@ -2,22 +2,20 @@
2
2
  // packages/policy-builder-cli/src/commands/synthesize.ts
3
3
  //
4
4
  // `policy-builder synthesize` subcommand. Dispatches to ONE of the two
5
- // front-ends (mandate / recording) based on which file flag is supplied:
6
- // --mandate <path.json> -> synthesizeFromMandate
5
+ // One front-end:
7
6
  // --recorded-tx <path.json> -> synthesizeFromRecording
8
7
  //
9
8
  // The CLI mirrors the MCP tool's discriminated union: one subcommand, two
10
9
  // front-ends, mutually exclusive.
11
10
  //
12
- // Per-field response flags (--window-seconds, --valid-until, --limit-amount,
13
- // --invocation-limit) merge into `userResponses`. A flag overrides the same
11
+ // Per-field response flags (--valid-until, --limit-amount) merge into
12
+ // `userResponses`. A flag overrides the same
14
13
  // field from --responses (CLI flags are explicit; the file is a default bag).
15
- // Oracle params (--oracle-max-staleness, --oracle-max-deviation) are part of
16
14
  // the interpreter opt-in and are rejected without --smart-account; tighten-only
17
15
  // bounds are validated by the core.
18
16
  //
19
17
  // --explain (Phase 1) makes the synthesised policy human-readable. The
20
- // orchestrator attaches the in-memory predicate tree + a SimulationResult
18
+ // orchestrator attaches the in-memory predicate tree
21
19
  // to the success envelope; the CLI builds the deterministic review card
22
20
  // from those inputs and emits it alongside the policy. Without --explain
23
21
  // the output is byte-identical to today.
@@ -32,12 +30,10 @@ const output_ts_1 = require("../output.js");
32
30
  const POSITIVE_INT_RE = /^[0-9]+$/;
33
31
  async function runSynthesizeCommand(argv, flags) {
34
32
  const pairs = (0, output_ts_1.parsePairs)(argv);
35
- const hasMandate = Boolean(pairs.mandate);
36
- const hasRecorded = Boolean(pairs['recorded-tx']);
37
- if (hasMandate === hasRecorded) {
33
+ if (!pairs['recorded-tx']) {
38
34
  throw new output_ts_1.CliError({
39
35
  code: 'CLI_MISSING_ARG',
40
- message: 'synthesize: exactly one of --mandate <path> or --recorded-tx <path> is required',
36
+ message: 'synthesize: --recorded-tx <path> is required',
41
37
  severity: 'error',
42
38
  retryable: false,
43
39
  });
@@ -48,16 +44,6 @@ async function runSynthesizeCommand(argv, flags) {
48
44
  // output gains `review` + `predicateTree` and remains unchanged
49
45
  // otherwise.
50
46
  const explain = pairs.explain !== undefined;
51
- if (hasMandate) {
52
- const mandate = (0, output_ts_1.readJsonFile)(pairs.mandate);
53
- const args = { source: 'mandate', mandate };
54
- applySharedFlags(args, pairs, explain);
55
- const res = await (0, run_1.runSynthesizePolicy)(args);
56
- if (explain)
57
- emitExplainBlock(res, flags);
58
- return (0, output_ts_1.formatToolResponse)(res, flags, 'synthesize(mandate)');
59
- }
60
- // hasRecorded
61
47
  const recordedFile = (0, output_ts_1.readJsonFile)(pairs['recorded-tx']);
62
48
  // Accept either a bare RecordedTransaction or the `{ ok, data }` artifact that
63
49
  // `record --out` writes (same shape as `--json`), so `record --out X` followed
@@ -93,10 +79,8 @@ async function runSynthesizeCommand(argv, flags) {
93
79
  // Per-field overrides. Each entry: argv flag name, userResponses key,
94
80
  // and a parser that validates the raw string.
95
81
  const userResponseFlags = [
96
- ['window-seconds', 'windowSeconds', parsePositiveInt],
97
82
  ['valid-until', 'validUntilLedger', parsePositiveInt],
98
83
  ['limit-amount', 'limitAmount', parseI128String],
99
- ['invocation-limit', 'invocationLimit', parsePositiveInt],
100
84
  ];
101
85
  for (const [flag, key, parse] of userResponseFlags) {
102
86
  if (pairs[flag] !== undefined) {
@@ -125,9 +109,10 @@ async function runSynthesizeCommand(argv, flags) {
125
109
  if (Object.keys(userResponses).length > 0)
126
110
  args.userResponses = userResponses;
127
111
  applySharedFlags(args, pairs, explain);
128
- // --smart-account <C...> opts into the interpreter adapter, so constraints OZ
129
- // cannot express (per-method scoping, invocation-count windows, oracle bounds,
130
- // exact hop paths) lower to a real predicate document instead of just warnings.
112
+ // --smart-account <C...> names the account the interpreter policy installs
113
+ // against. It is what lets the recording lower to a real predicate document
114
+ // (amount caps, recipient allowlists, exact hop paths) rather than warnings;
115
+ // without it the core reports that no installable policy was synthesised.
131
116
  // The core validates the address and installNonce; a bad value surfaces there.
132
117
  //
133
118
  // Use `!== undefined` (not truthy) so `--smart-account ""` and `--install-nonce`
@@ -137,9 +122,6 @@ async function runSynthesizeCommand(argv, flags) {
137
122
  // when it had been silently skipped.
138
123
  const smartAccountRaw = pairs['smart-account'];
139
124
  const installNonceRaw = pairs['install-nonce'];
140
- const oracleStalenessRaw = pairs['oracle-max-staleness'];
141
- const oracleDeviationRaw = pairs['oracle-max-deviation'];
142
- // --install-nonce and the oracle knobs are interpreter-only. Reject up
143
125
  // front so they cannot be silently dropped when --smart-account is absent.
144
126
  if (smartAccountRaw === undefined) {
145
127
  if (installNonceRaw !== undefined) {
@@ -150,14 +132,6 @@ async function runSynthesizeCommand(argv, flags) {
150
132
  retryable: false,
151
133
  });
152
134
  }
153
- if (oracleStalenessRaw !== undefined || oracleDeviationRaw !== undefined) {
154
- throw new output_ts_1.CliError({
155
- code: 'CLI_MISSING_ARG',
156
- message: 'synthesize: --oracle-max-staleness / --oracle-max-deviation require --smart-account <C...> (interpreter opt-in)',
157
- severity: 'error',
158
- retryable: false,
159
- });
160
- }
161
135
  }
162
136
  else {
163
137
  const smartAccount = smartAccountRaw.trim();
@@ -190,19 +164,8 @@ async function runSynthesizeCommand(argv, flags) {
190
164
  }
191
165
  interpreter.installNonce = nonce;
192
166
  }
193
- // Oracle params only attach when at least one bound was provided. The
194
167
  // core validates tighten-only (maxStalenessSeconds <= 600,
195
168
  // maxDeviationBps <= 200) - a too-loose value surfaces as SYNTHESIS_ERROR.
196
- if (oracleStalenessRaw !== undefined || oracleDeviationRaw !== undefined) {
197
- const oracleParams = {};
198
- if (oracleStalenessRaw !== undefined) {
199
- oracleParams.maxStalenessSeconds = parsePositiveInt(oracleStalenessRaw, '--oracle-max-staleness');
200
- }
201
- if (oracleDeviationRaw !== undefined) {
202
- oracleParams.maxDeviationBps = parsePositiveInt(oracleDeviationRaw, '--oracle-max-deviation');
203
- }
204
- interpreter.oracleParams = oracleParams;
205
- }
206
169
  args.interpreter = interpreter;
207
170
  }
208
171
  if (explain)
@@ -216,13 +179,9 @@ async function runSynthesizeCommand(argv, flags) {
216
179
  emitExplainBlock(res, flags);
217
180
  return (0, output_ts_1.formatToolResponse)(res, flags, 'synthesize(recording)');
218
181
  }
219
- /** Apply flags shared between the mandate and recording synthesize paths:
220
- * --oz-config, --confidence, --explain. Pulled out so adding a shared flag
221
- * adds one branch here rather than two copies in the command body. */
182
+ /** Apply the flags that are not part of the recording payload itself:
183
+ * --confidence, --explain. */
222
184
  function applySharedFlags(args, pairs, explain) {
223
- if (pairs['oz-config'] !== undefined) {
224
- args.ozConfig = readOzConfigFile(pairs['oz-config']);
225
- }
226
185
  if (pairs.confidence !== undefined) {
227
186
  args.confidenceOverride = { threshold: parseConfidence(pairs.confidence) };
228
187
  }
@@ -242,15 +201,15 @@ function emitExplainBlock(res, flags) {
242
201
  const review = (0, policy_synth_1.buildReviewCardSummary)(
243
202
  // The orchestrator's `predicateTree` is the exact in-memory AST
244
203
  // (canonical JSON shape). The builder's input is typed as
245
- // `PredicateNode | null`; the orchestrator's `null` is the truthful
246
- // OZ-only / mandate value, so a null here is honest.
247
- (res.explain.predicateTree ?? null), res.data?.policyRefs ?? [], res.data?.contextRule ?? {
204
+ // `PredicateNode | null`; a null is the truthful value when the
205
+ // interpreter adapter was not engaged.
206
+ (res.explain.predicateTree ?? null), res.data?.contextRule ?? {
248
207
  contextRuleType: { kind: 'default' },
249
208
  name: 'unknown',
250
209
  validUntilLedger: null,
251
210
  signers: [],
252
211
  policies: [],
253
- }, res.explain.simulation);
212
+ }, 'interpreter-v1');
254
213
  res.data.review =
255
214
  review;
256
215
  res.data.predicateTree = res.explain.predicateTree;
@@ -272,23 +231,6 @@ function emitExplainBlock(res, flags) {
272
231
  }
273
232
  }
274
233
  }
275
- /** Read and validate an OzAdapterConfig JSON file. Throws CLI_FILE_NOT_FOUND /
276
- * CLI_INVALID_JSON for filesystem / parse failures; the core's strict schema
277
- * on `ozConfig` catches shape mismatches downstream. */
278
- function readOzConfigFile(path) {
279
- const value = (0, output_ts_1.readJsonFile)(path);
280
- if (value === null || typeof value !== 'object' || Array.isArray(value)) {
281
- throw new output_ts_1.CliError({
282
- code: 'CLI_INVALID_JSON',
283
- message: `synthesize: --oz-config ${path} must be a JSON object`,
284
- severity: 'error',
285
- retryable: false,
286
- });
287
- }
288
- return value;
289
- }
290
- /** Parse and validate `--confidence <n>` as a finite number in [0, 1]. A
291
- * threshold above 1 would disable the recorder gate; reject it up front. */
292
234
  function parseConfidence(raw) {
293
235
  const n = Number(raw);
294
236
  if (!Number.isFinite(n) || n < 0 || n > 1) {
@@ -301,8 +243,7 @@ function parseConfidence(raw) {
301
243
  }
302
244
  return n;
303
245
  }
304
- /** Parse a strictly positive integer (windowSeconds, validUntilLedger,
305
- * invocationLimit, oracleParams bounds). The core re-validates these with
246
+ /** Parse a strictly positive integer (validUntilLedger,
306
247
  * field-specific caps; the CLI just enforces "looks like an integer > 0".
307
248
  * The `^[0-9]+$` regex already pins the shape to a non-negative integer, so
308
249
  * the only thing left to check is "not zero". */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crediolabs/policy-builder-cli",
3
- "version": "0.1.18",
3
+ "version": "0.3.0",
4
4
  "license": "MIT",
5
5
  "description": "CLI wrapper around the OZ policy-synth core (record + synthesize) for solo-dev and CI workflows.",
6
6
  "type": "module",
@@ -12,12 +12,12 @@
12
12
  },
13
13
  "repository": {
14
14
  "type": "git",
15
- "url": "https://github.com/untangledfinance/octogate.git",
15
+ "url": "https://github.com/untangledfinance/oz-policy-builder.git",
16
16
  "directory": "packages/policy-builder-cli"
17
17
  },
18
- "homepage": "https://github.com/untangledfinance/octogate#readme",
18
+ "homepage": "https://github.com/untangledfinance/oz-policy-builder#readme",
19
19
  "bugs": {
20
- "url": "https://github.com/untangledfinance/octogate/issues"
20
+ "url": "https://github.com/untangledfinance/oz-policy-builder/issues"
21
21
  },
22
22
  "keywords": [
23
23
  "stellar",
@@ -63,7 +63,7 @@
63
63
  "prepack": "bun run build"
64
64
  },
65
65
  "dependencies": {
66
- "@crediolabs/policy-synth": "0.1.18",
66
+ "@crediolabs/policy-synth": "0.3.0",
67
67
  "@stellar/stellar-sdk": "14.4.0",
68
68
  "zod": "3.25.76"
69
69
  },
@@ -1,22 +1,20 @@
1
1
  // packages/policy-builder-cli/src/commands/synthesize.ts
2
2
  //
3
3
  // `policy-builder synthesize` subcommand. Dispatches to ONE of the two
4
- // front-ends (mandate / recording) based on which file flag is supplied:
5
- // --mandate <path.json> -> synthesizeFromMandate
4
+ // One front-end:
6
5
  // --recorded-tx <path.json> -> synthesizeFromRecording
7
6
  //
8
7
  // The CLI mirrors the MCP tool's discriminated union: one subcommand, two
9
8
  // front-ends, mutually exclusive.
10
9
  //
11
- // Per-field response flags (--window-seconds, --valid-until, --limit-amount,
12
- // --invocation-limit) merge into `userResponses`. A flag overrides the same
10
+ // Per-field response flags (--valid-until, --limit-amount) merge into
11
+ // `userResponses`. A flag overrides the same
13
12
  // field from --responses (CLI flags are explicit; the file is a default bag).
14
- // Oracle params (--oracle-max-staleness, --oracle-max-deviation) are part of
15
13
  // the interpreter opt-in and are rejected without --smart-account; tighten-only
16
14
  // bounds are validated by the core.
17
15
  //
18
16
  // --explain (Phase 1) makes the synthesised policy human-readable. The
19
- // orchestrator attaches the in-memory predicate tree + a SimulationResult
17
+ // orchestrator attaches the in-memory predicate tree
20
18
  // to the success envelope; the CLI builds the deterministic review card
21
19
  // from those inputs and emits it alongside the policy. Without --explain
22
20
  // the output is byte-identical to today.
@@ -30,17 +28,6 @@ import {
30
28
  import { runSynthesizePolicy } from '@crediolabs/policy-synth/run'
31
29
  import { CliError, type CliFlags, formatToolResponse, parsePairs, readJsonFile } from '../output.ts'
32
30
 
33
- /** Local mirror of the synth core's `SimulationResult` shape. The verify
34
- * module is internal to the package, so the CLI keeps this minimal copy
35
- * instead of importing the deep path - the contract is small enough that
36
- * a structural type is cheaper than a new package export. */
37
- type SimulationResult = {
38
- permit: { tx: 'permit' } | { tx: 'deny'; reason: string }
39
- evaluatedCases: Array<{ dimension: string; outcome: 'permit' | 'deny'; reason: string }>
40
- backend: 'interpreter-v1' | 'ts-model'
41
- simulatorVersion: string
42
- }
43
-
44
31
  // Positive-int flags and i128 amount strings share the same wire shape: a
45
32
  // base-10 unsigned decimal, no sign. The i128 stays a string at the boundary
46
33
  // because it is wider than Number.MAX_SAFE_INTEGER.
@@ -51,12 +38,10 @@ export async function runSynthesizeCommand(
51
38
  flags: CliFlags
52
39
  ): Promise<ProposedPolicy> {
53
40
  const pairs = parsePairs(argv)
54
- const hasMandate = Boolean(pairs.mandate)
55
- const hasRecorded = Boolean(pairs['recorded-tx'])
56
- if (hasMandate === hasRecorded) {
41
+ if (!pairs['recorded-tx']) {
57
42
  throw new CliError({
58
43
  code: 'CLI_MISSING_ARG',
59
- message: 'synthesize: exactly one of --mandate <path> or --recorded-tx <path> is required',
44
+ message: 'synthesize: --recorded-tx <path> is required',
60
45
  severity: 'error',
61
46
  retryable: false,
62
47
  })
@@ -69,16 +54,6 @@ export async function runSynthesizeCommand(
69
54
  // otherwise.
70
55
  const explain = pairs.explain !== undefined
71
56
 
72
- if (hasMandate) {
73
- const mandate = readJsonFile(pairs.mandate as string) as Record<string, unknown>
74
- const args: Record<string, unknown> = { source: 'mandate', mandate }
75
- applySharedFlags(args, pairs, explain)
76
- const res = await runSynthesizePolicy(args)
77
- if (explain) emitExplainBlock(res, flags)
78
- return formatToolResponse(res, flags, 'synthesize(mandate)')
79
- }
80
-
81
- // hasRecorded
82
57
  const recordedFile = readJsonFile(pairs['recorded-tx'] as string) as Record<string, unknown>
83
58
  // Accept either a bare RecordedTransaction or the `{ ok, data }` artifact that
84
59
  // `record --out` writes (same shape as `--json`), so `record --out X` followed
@@ -116,10 +91,8 @@ export async function runSynthesizeCommand(
116
91
  // Per-field overrides. Each entry: argv flag name, userResponses key,
117
92
  // and a parser that validates the raw string.
118
93
  const userResponseFlags: Array<[string, string, (raw: string, flag: string) => unknown]> = [
119
- ['window-seconds', 'windowSeconds', parsePositiveInt],
120
94
  ['valid-until', 'validUntilLedger', parsePositiveInt],
121
95
  ['limit-amount', 'limitAmount', parseI128String],
122
- ['invocation-limit', 'invocationLimit', parsePositiveInt],
123
96
  ]
124
97
  for (const [flag, key, parse] of userResponseFlags) {
125
98
  if (pairs[flag] !== undefined) {
@@ -148,9 +121,10 @@ export async function runSynthesizeCommand(
148
121
 
149
122
  applySharedFlags(args, pairs, explain)
150
123
 
151
- // --smart-account <C...> opts into the interpreter adapter, so constraints OZ
152
- // cannot express (per-method scoping, invocation-count windows, oracle bounds,
153
- // exact hop paths) lower to a real predicate document instead of just warnings.
124
+ // --smart-account <C...> names the account the interpreter policy installs
125
+ // against. It is what lets the recording lower to a real predicate document
126
+ // (amount caps, recipient allowlists, exact hop paths) rather than warnings;
127
+ // without it the core reports that no installable policy was synthesised.
154
128
  // The core validates the address and installNonce; a bad value surfaces there.
155
129
  //
156
130
  // Use `!== undefined` (not truthy) so `--smart-account ""` and `--install-nonce`
@@ -160,9 +134,6 @@ export async function runSynthesizeCommand(
160
134
  // when it had been silently skipped.
161
135
  const smartAccountRaw = pairs['smart-account']
162
136
  const installNonceRaw = pairs['install-nonce']
163
- const oracleStalenessRaw = pairs['oracle-max-staleness']
164
- const oracleDeviationRaw = pairs['oracle-max-deviation']
165
- // --install-nonce and the oracle knobs are interpreter-only. Reject up
166
137
  // front so they cannot be silently dropped when --smart-account is absent.
167
138
  if (smartAccountRaw === undefined) {
168
139
  if (installNonceRaw !== undefined) {
@@ -173,15 +144,6 @@ export async function runSynthesizeCommand(
173
144
  retryable: false,
174
145
  })
175
146
  }
176
- if (oracleStalenessRaw !== undefined || oracleDeviationRaw !== undefined) {
177
- throw new CliError({
178
- code: 'CLI_MISSING_ARG',
179
- message:
180
- 'synthesize: --oracle-max-staleness / --oracle-max-deviation require --smart-account <C...> (interpreter opt-in)',
181
- severity: 'error',
182
- retryable: false,
183
- })
184
- }
185
147
  } else {
186
148
  const smartAccount = smartAccountRaw.trim()
187
149
  if (smartAccount.length === 0) {
@@ -214,25 +176,8 @@ export async function runSynthesizeCommand(
214
176
  }
215
177
  interpreter.installNonce = nonce
216
178
  }
217
- // Oracle params only attach when at least one bound was provided. The
218
179
  // core validates tighten-only (maxStalenessSeconds <= 600,
219
180
  // maxDeviationBps <= 200) - a too-loose value surfaces as SYNTHESIS_ERROR.
220
- if (oracleStalenessRaw !== undefined || oracleDeviationRaw !== undefined) {
221
- const oracleParams: Record<string, number> = {}
222
- if (oracleStalenessRaw !== undefined) {
223
- oracleParams.maxStalenessSeconds = parsePositiveInt(
224
- oracleStalenessRaw,
225
- '--oracle-max-staleness'
226
- )
227
- }
228
- if (oracleDeviationRaw !== undefined) {
229
- oracleParams.maxDeviationBps = parsePositiveInt(
230
- oracleDeviationRaw,
231
- '--oracle-max-deviation'
232
- )
233
- }
234
- interpreter.oracleParams = oracleParams
235
- }
236
181
  args.interpreter = interpreter
237
182
  }
238
183
  if (explain) args.explain = true
@@ -245,17 +190,13 @@ export async function runSynthesizeCommand(
245
190
  return formatToolResponse(res, flags, 'synthesize(recording)')
246
191
  }
247
192
 
248
- /** Apply flags shared between the mandate and recording synthesize paths:
249
- * --oz-config, --confidence, --explain. Pulled out so adding a shared flag
250
- * adds one branch here rather than two copies in the command body. */
193
+ /** Apply the flags that are not part of the recording payload itself:
194
+ * --confidence, --explain. */
251
195
  function applySharedFlags(
252
196
  args: Record<string, unknown>,
253
197
  pairs: Record<string, string>,
254
198
  explain: boolean
255
199
  ): void {
256
- if (pairs['oz-config'] !== undefined) {
257
- args.ozConfig = readOzConfigFile(pairs['oz-config'] as string)
258
- }
259
200
  if (pairs.confidence !== undefined) {
260
201
  args.confidenceOverride = { threshold: parseConfidence(pairs.confidence as string) }
261
202
  }
@@ -275,7 +216,6 @@ function emitExplainBlock(
275
216
  data?: ProposedPolicy
276
217
  explain?: {
277
218
  predicateTree: unknown
278
- simulation: SimulationResult
279
219
  }
280
220
  },
281
221
  flags: CliFlags
@@ -284,10 +224,9 @@ function emitExplainBlock(
284
224
  const review = buildReviewCardSummary(
285
225
  // The orchestrator's `predicateTree` is the exact in-memory AST
286
226
  // (canonical JSON shape). The builder's input is typed as
287
- // `PredicateNode | null`; the orchestrator's `null` is the truthful
288
- // OZ-only / mandate value, so a null here is honest.
227
+ // `PredicateNode | null`; a null is the truthful value when the
228
+ // interpreter adapter was not engaged.
289
229
  (res.explain.predicateTree ?? null) as never,
290
- res.data?.policyRefs ?? [],
291
230
  res.data?.contextRule ?? {
292
231
  contextRuleType: { kind: 'default' as const },
293
232
  name: 'unknown',
@@ -295,7 +234,7 @@ function emitExplainBlock(
295
234
  signers: [],
296
235
  policies: [],
297
236
  },
298
- res.explain.simulation
237
+ 'interpreter-v1'
299
238
  )
300
239
  // Attach the two additive fields to the on-wire envelope. formatToolResponse
301
240
  // reads from `res` and writes the JSON; we mutate the same object so the
@@ -324,24 +263,6 @@ function emitExplainBlock(
324
263
  }
325
264
  }
326
265
 
327
- /** Read and validate an OzAdapterConfig JSON file. Throws CLI_FILE_NOT_FOUND /
328
- * CLI_INVALID_JSON for filesystem / parse failures; the core's strict schema
329
- * on `ozConfig` catches shape mismatches downstream. */
330
- function readOzConfigFile(path: string): Record<string, unknown> {
331
- const value = readJsonFile(path)
332
- if (value === null || typeof value !== 'object' || Array.isArray(value)) {
333
- throw new CliError({
334
- code: 'CLI_INVALID_JSON',
335
- message: `synthesize: --oz-config ${path} must be a JSON object`,
336
- severity: 'error',
337
- retryable: false,
338
- })
339
- }
340
- return value as Record<string, unknown>
341
- }
342
-
343
- /** Parse and validate `--confidence <n>` as a finite number in [0, 1]. A
344
- * threshold above 1 would disable the recorder gate; reject it up front. */
345
266
  function parseConfidence(raw: string): number {
346
267
  const n = Number(raw)
347
268
  if (!Number.isFinite(n) || n < 0 || n > 1) {
@@ -355,8 +276,7 @@ function parseConfidence(raw: string): number {
355
276
  return n
356
277
  }
357
278
 
358
- /** Parse a strictly positive integer (windowSeconds, validUntilLedger,
359
- * invocationLimit, oracleParams bounds). The core re-validates these with
279
+ /** Parse a strictly positive integer (validUntilLedger,
360
280
  * field-specific caps; the CLI just enforces "looks like an integer > 0".
361
281
  * The `^[0-9]+$` regex already pins the shape to a non-negative integer, so
362
282
  * the only thing left to check is "not zero". */