@crediolabs/policy-builder-cli 0.1.16 → 0.1.18
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 +39 -104
- package/dist/bin/policy-builder.js +1 -1
- package/dist/src/commands/record.js +1 -1
- package/dist/src/commands/synthesize.js +70 -78
- package/dist/src/index.js +1 -1
- package/dist/src/output.js +13 -14
- package/dist-cjs/src/commands/record.js +1 -1
- package/dist-cjs/src/commands/synthesize.js +70 -78
- package/dist-cjs/src/index.js +1 -1
- package/dist-cjs/src/output.js +13 -14
- package/package.json +6 -6
- package/src/commands/record.ts +1 -1
- package/src/commands/synthesize.ts +74 -90
- package/src/index.ts +1 -1
- package/src/output.ts +10 -12
package/README.md
CHANGED
|
@@ -1,132 +1,67 @@
|
|
|
1
1
|
# @crediolabs/policy-builder-cli
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Command-line wrapper around
|
|
4
|
+
[`@crediolabs/policy-synth`](https://www.npmjs.com/package/@crediolabs/policy-synth):
|
|
5
|
+
record a Soroban transaction and synthesise the minimal policy that permits
|
|
6
|
+
exactly that flow, from a shell or a CI job.
|
|
6
7
|
|
|
7
8
|
## Install
|
|
8
9
|
|
|
9
10
|
```sh
|
|
10
11
|
npm install -g @crediolabs/policy-builder-cli
|
|
11
|
-
# or run without installing
|
|
12
|
-
|
|
12
|
+
# or run without installing
|
|
13
|
+
bunx @crediolabs/policy-builder-cli record --network testnet --hash <tx>
|
|
13
14
|
```
|
|
14
15
|
|
|
16
|
+
The binary is `policy-builder`.
|
|
17
|
+
|
|
15
18
|
## Commands
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
policy-builder record --network <mainnet|testnet> --hash <tx> | --xdr <b64>
|
|
19
|
-
[--json] [--quiet] [--out <path>]
|
|
20
|
-
|
|
21
|
-
policy-builder synthesize --mandate <path.json>
|
|
22
|
-
[--oz-config <path.json>] [--confidence <0..1>]
|
|
23
|
-
[--json] [--quiet] [--out <path>]
|
|
24
|
-
|
|
25
|
-
policy-builder synthesize --recorded-tx <path.json> --network <mainnet|testnet>
|
|
26
|
-
[--responses <path.json>]
|
|
27
|
-
[--oz-config <path.json>] [--confidence <0..1>]
|
|
28
|
-
[--smart-account <C...>] [--install-nonce <n>]
|
|
29
|
-
[--oracle-max-staleness <s>] [--oracle-max-deviation <bps>]
|
|
30
|
-
[--window-seconds <n>] [--valid-until <ledger>]
|
|
31
|
-
[--limit-amount <i128str>] [--invocation-limit <n>]
|
|
32
|
-
[--json] [--quiet] [--out <path>]
|
|
33
|
-
```
|
|
20
|
+
### `record`
|
|
34
21
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
- `--json` — emit a machine-readable JSON envelope (`{ ok, data }` / `{ ok, error }`) on stdout.
|
|
38
|
-
- `--quiet` — suppress progress / non-error output.
|
|
39
|
-
- `--out <path>` — write the artefact to a file (JSON).
|
|
40
|
-
- `--oz-config <path.json>` — JSON `OzAdapterConfig` (custom OZ instance
|
|
41
|
-
addresses for the network). Defaults to the core's mainnet placeholder.
|
|
42
|
-
Applies to both the `--mandate` and `--recorded-tx` synthesize paths.
|
|
43
|
-
- `--confidence <0..1>` — recorder confidence gate threshold. The synth
|
|
44
|
-
rejects when `parseConfidence.overall < threshold`; it does not change the
|
|
45
|
-
recording's `parseConfidence.thresholdUsed` in the output. Must be a finite
|
|
46
|
-
number in `[0, 1]` (inclusive); out-of-range values are rejected with
|
|
47
|
-
`CLI_MISSING_ARG`.
|
|
48
|
-
- `--smart-account <C...>` — opt into the interpreter adapter, so
|
|
49
|
-
constraints OZ built-ins cannot express (per-method scoping, invocation-count
|
|
50
|
-
windows, oracle bounds, exact hop paths) lower to a real predicate document
|
|
51
|
-
instead of warnings. Must be a Stellar contract strkey (56 chars, base32
|
|
52
|
-
alphabet `[2-7A-Z]`, matching `^C[2-7A-Z]{55}$`).
|
|
53
|
-
- `--install-nonce <n>` — per-rule install nonce for the interpreter policy.
|
|
54
|
-
Requires `--smart-account`; the CLI rejects `--install-nonce` without
|
|
55
|
-
`--smart-account` so callers cannot silently drop the nonce.
|
|
56
|
-
- `--oracle-max-staleness <s>` — interpreter oracle max staleness in seconds
|
|
57
|
-
(positive integer `<= 600`, tighten-only vs the wasm default). Requires
|
|
58
|
-
`--smart-account`; widen values are rejected by the core.
|
|
59
|
-
- `--oracle-max-deviation <bps>` — interpreter oracle max deviation in basis
|
|
60
|
-
points (positive integer `<= 200`, tighten-only). Requires
|
|
61
|
-
`--smart-account`; widen values are rejected by the core.
|
|
62
|
-
- `--window-seconds <n>` — per-field override of `userResponses.windowSeconds`.
|
|
63
|
-
Overrides the same field from `--responses` (CLI flag wins).
|
|
64
|
-
- `--valid-until <ledger>` — per-field override of `userResponses.validUntilLedger`.
|
|
65
|
-
Overrides the same field from `--responses` (CLI flag wins).
|
|
66
|
-
- `--limit-amount <i128str>` — per-field override of `userResponses.limitAmount`
|
|
67
|
-
(positive base-10 i128 string). Overrides the same field from `--responses`.
|
|
68
|
-
- `--invocation-limit <n>` — per-field override of `userResponses.invocationLimit`.
|
|
69
|
-
Overrides the same field from `--responses` (CLI flag wins).
|
|
70
|
-
|
|
71
|
-
**Precedence for per-field response flags:** `--responses <file>` provides the
|
|
72
|
-
base `userResponses` object; any of the per-field flags above override the
|
|
73
|
-
same-named field from the file (the CLI flag is the explicit caller intent,
|
|
74
|
-
the file is the default bag). The two never merge - either-or on the same
|
|
75
|
-
field.
|
|
76
|
-
|
|
77
|
-
A `ToolError` exits non-zero (`1`), so CI scripts can gate on `$?`. Empty
|
|
78
|
-
`--smart-account`, out-of-range `--confidence`, non-numeric `--window-seconds`,
|
|
79
|
-
non-integer `--oracle-*`, and `--oracle-*` without `--smart-account` are all
|
|
80
|
-
rejected with `CLI_MISSING_ARG` before the call reaches the core.
|
|
81
|
-
|
|
82
|
-
## Examples
|
|
83
|
-
|
|
84
|
-
Record an on-chain transaction and synthesise from the recording:
|
|
22
|
+
Decode a transaction into a `RecordedTransaction`:
|
|
85
23
|
|
|
86
24
|
```sh
|
|
87
|
-
policy-builder record --network mainnet --hash <
|
|
88
|
-
policy-builder
|
|
25
|
+
policy-builder record --network mainnet --hash <transaction hash>
|
|
26
|
+
policy-builder record --network testnet --xdr <base64 envelope XDR>
|
|
89
27
|
```
|
|
90
28
|
|
|
91
|
-
|
|
92
|
-
|
|
29
|
+
Exactly one of `--hash` (fetched over RPC) or `--xdr` (decoded locally) is
|
|
30
|
+
required.
|
|
31
|
+
|
|
32
|
+
### `synthesize`
|
|
33
|
+
|
|
34
|
+
Produce a `ProposedPolicy` from a recording or from a mandate file (exactly
|
|
35
|
+
one of the two):
|
|
93
36
|
|
|
94
37
|
```sh
|
|
95
|
-
policy-builder synthesize
|
|
96
|
-
|
|
97
|
-
--network mainnet \
|
|
98
|
-
--responses responses.json \
|
|
99
|
-
--smart-account CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIT4C \
|
|
100
|
-
--install-nonce 1 \
|
|
101
|
-
--json
|
|
38
|
+
policy-builder synthesize --network mainnet --recorded-tx recorded.json
|
|
39
|
+
policy-builder synthesize --mandate mandate.json
|
|
102
40
|
```
|
|
103
41
|
|
|
104
|
-
|
|
42
|
+
`synthesize --recorded-tx` accepts either a bare `RecordedTransaction` or the
|
|
43
|
+
artifact `record --out` writes, so `record --out tx.json` followed by
|
|
44
|
+
`synthesize --recorded-tx tx.json` works end to end. Add `--explain` to
|
|
45
|
+
include the human-readable review card and predicate tree in the output.
|
|
105
46
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
"contract": "CTOKEN",
|
|
110
|
-
"method": "transfer",
|
|
111
|
-
"spendingLimit": { "token": "CTOKEN", "limit": "5000000", "windowSeconds": 2592000 }
|
|
112
|
-
}
|
|
113
|
-
```
|
|
47
|
+
Both commands print a single JSON envelope to stdout: the result on success,
|
|
48
|
+
a machine-readable error object (stable `code`, `severity`, `retryable`) on
|
|
49
|
+
failure. That makes the output safe to pipe and to assert on in CI.
|
|
114
50
|
|
|
115
|
-
|
|
116
|
-
policy-builder synthesize --mandate mandate.json --json
|
|
117
|
-
```
|
|
51
|
+
## Beyond record and synthesize
|
|
118
52
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
53
|
+
Verification, simulation, install and revoke are part of the same core; drive
|
|
54
|
+
them from
|
|
55
|
+
[`@crediolabs/policy-builder-mcp`](https://www.npmjs.com/package/@crediolabs/policy-builder-mcp)
|
|
56
|
+
or call `@crediolabs/policy-synth/run` directly. The CLI stays deliberately
|
|
57
|
+
small: the two commands that fit a pipe.
|
|
123
58
|
|
|
124
|
-
##
|
|
59
|
+
## Security model
|
|
125
60
|
|
|
126
|
-
The
|
|
127
|
-
|
|
128
|
-
|
|
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).
|
|
129
64
|
|
|
130
65
|
## License
|
|
131
66
|
|
|
132
|
-
MIT
|
|
67
|
+
MIT
|
|
@@ -1,4 +1,4 @@
|
|
|
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
4
|
// front-ends (mandate / recording) based on which file flag is supplied:
|
|
@@ -48,14 +48,7 @@ export async function runSynthesizeCommand(argv, flags) {
|
|
|
48
48
|
if (hasMandate) {
|
|
49
49
|
const mandate = readJsonFile(pairs.mandate);
|
|
50
50
|
const args = { source: 'mandate', mandate };
|
|
51
|
-
|
|
52
|
-
args.ozConfig = readOzConfigFile(pairs['oz-config']);
|
|
53
|
-
}
|
|
54
|
-
if (pairs.confidence !== undefined) {
|
|
55
|
-
args.confidenceOverride = { threshold: parseConfidence(pairs.confidence) };
|
|
56
|
-
}
|
|
57
|
-
if (explain)
|
|
58
|
-
args.explain = true;
|
|
51
|
+
applySharedFlags(args, pairs, explain);
|
|
59
52
|
const res = await runSynthesizePolicy(args);
|
|
60
53
|
if (explain)
|
|
61
54
|
emitExplainBlock(res, flags);
|
|
@@ -84,10 +77,7 @@ export async function runSynthesizeCommand(argv, flags) {
|
|
|
84
77
|
const userResponses = {};
|
|
85
78
|
if (pairs.responses) {
|
|
86
79
|
const file = readJsonFile(pairs.responses);
|
|
87
|
-
if (file
|
|
88
|
-
Object.assign(userResponses, file);
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
80
|
+
if (file === null || typeof file !== 'object' || Array.isArray(file)) {
|
|
91
81
|
throw new CliError({
|
|
92
82
|
code: 'CLI_INVALID_JSON',
|
|
93
83
|
message: `synthesize: --responses ${pairs.responses} must be a JSON object`,
|
|
@@ -95,48 +85,43 @@ export async function runSynthesizeCommand(argv, flags) {
|
|
|
95
85
|
retryable: false,
|
|
96
86
|
});
|
|
97
87
|
}
|
|
88
|
+
Object.assign(userResponses, file);
|
|
98
89
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
90
|
+
// Per-field overrides. Each entry: argv flag name, userResponses key,
|
|
91
|
+
// and a parser that validates the raw string.
|
|
92
|
+
const userResponseFlags = [
|
|
93
|
+
['window-seconds', 'windowSeconds', parsePositiveInt],
|
|
94
|
+
['valid-until', 'validUntilLedger', parsePositiveInt],
|
|
95
|
+
['limit-amount', 'limitAmount', parseI128String],
|
|
96
|
+
['invocation-limit', 'invocationLimit', parsePositiveInt],
|
|
97
|
+
];
|
|
98
|
+
for (const [flag, key, parse] of userResponseFlags) {
|
|
99
|
+
if (pairs[flag] !== undefined) {
|
|
100
|
+
userResponses[key] = parse(pairs[flag], `--${flag}`);
|
|
101
|
+
}
|
|
110
102
|
}
|
|
111
103
|
// --recipient <C...|G...> is REPEATABLE (parsePairs collapses duplicate keys,
|
|
112
104
|
// so it is collected straight from argv). Each value builds the swap-recipient
|
|
113
105
|
// allowlist; supplying it REPLACES the default pin to the recorded recipient.
|
|
114
106
|
const recipients = collectRepeated(argv, 'recipient');
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
});
|
|
127
|
-
}
|
|
107
|
+
for (const r of recipients) {
|
|
108
|
+
// Same validator the run-layer schema applies (SDK StrKey underneath) - a
|
|
109
|
+
// swap recipient may be a G... wallet or a C... contract. Shared rather
|
|
110
|
+
// than re-inlined so the CLI and the schema cannot drift apart.
|
|
111
|
+
if (!isStellarAddress(r)) {
|
|
112
|
+
throw new CliError({
|
|
113
|
+
code: 'CLI_MISSING_ARG',
|
|
114
|
+
message: `synthesize: --recipient "${r}" is not a valid Stellar address (expected a G... wallet or C... contract)`,
|
|
115
|
+
severity: 'error',
|
|
116
|
+
retryable: false,
|
|
117
|
+
});
|
|
128
118
|
}
|
|
129
|
-
userResponses.swapRecipientAllowlist = recipients;
|
|
130
119
|
}
|
|
131
|
-
if (
|
|
120
|
+
if (recipients.length > 0)
|
|
121
|
+
userResponses.swapRecipientAllowlist = recipients;
|
|
122
|
+
if (Object.keys(userResponses).length > 0)
|
|
132
123
|
args.userResponses = userResponses;
|
|
133
|
-
|
|
134
|
-
if (pairs['oz-config'] !== undefined) {
|
|
135
|
-
args.ozConfig = readOzConfigFile(pairs['oz-config']);
|
|
136
|
-
}
|
|
137
|
-
if (pairs.confidence !== undefined) {
|
|
138
|
-
args.confidenceOverride = { threshold: parseConfidence(pairs.confidence) };
|
|
139
|
-
}
|
|
124
|
+
applySharedFlags(args, pairs, explain);
|
|
140
125
|
// --smart-account <C...> opts into the interpreter adapter, so constraints OZ
|
|
141
126
|
// cannot express (per-method scoping, invocation-count windows, oracle bounds,
|
|
142
127
|
// exact hop paths) lower to a real predicate document instead of just warnings.
|
|
@@ -146,31 +131,32 @@ export async function runSynthesizeCommand(argv, flags) {
|
|
|
146
131
|
// without `--smart-account` are rejected up front instead of being silently
|
|
147
132
|
// dropped. The foot-gun: an empty value previously produced an "ok" envelope
|
|
148
133
|
// with 0 policyDocuments, so callers thought the constraint had been enforced
|
|
149
|
-
// when
|
|
134
|
+
// when it had been silently skipped.
|
|
150
135
|
const smartAccountRaw = pairs['smart-account'];
|
|
151
136
|
const installNonceRaw = pairs['install-nonce'];
|
|
152
137
|
const oracleStalenessRaw = pairs['oracle-max-staleness'];
|
|
153
138
|
const oracleDeviationRaw = pairs['oracle-max-deviation'];
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
139
|
+
// --install-nonce and the oracle knobs are interpreter-only. Reject up
|
|
140
|
+
// front so they cannot be silently dropped when --smart-account is absent.
|
|
141
|
+
if (smartAccountRaw === undefined) {
|
|
142
|
+
if (installNonceRaw !== undefined) {
|
|
143
|
+
throw new CliError({
|
|
144
|
+
code: 'CLI_MISSING_ARG',
|
|
145
|
+
message: 'synthesize: --install-nonce requires --smart-account <C...> (interpreter opt-in)',
|
|
146
|
+
severity: 'error',
|
|
147
|
+
retryable: false,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
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
|
+
}
|
|
172
158
|
}
|
|
173
|
-
|
|
159
|
+
else {
|
|
174
160
|
const smartAccount = smartAccountRaw.trim();
|
|
175
161
|
if (smartAccount.length === 0) {
|
|
176
162
|
throw new CliError({
|
|
@@ -227,6 +213,19 @@ export async function runSynthesizeCommand(argv, flags) {
|
|
|
227
213
|
emitExplainBlock(res, flags);
|
|
228
214
|
return formatToolResponse(res, flags, 'synthesize(recording)');
|
|
229
215
|
}
|
|
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. */
|
|
219
|
+
function applySharedFlags(args, pairs, explain) {
|
|
220
|
+
if (pairs['oz-config'] !== undefined) {
|
|
221
|
+
args.ozConfig = readOzConfigFile(pairs['oz-config']);
|
|
222
|
+
}
|
|
223
|
+
if (pairs.confidence !== undefined) {
|
|
224
|
+
args.confidenceOverride = { threshold: parseConfidence(pairs.confidence) };
|
|
225
|
+
}
|
|
226
|
+
if (explain)
|
|
227
|
+
args.explain = true;
|
|
228
|
+
}
|
|
230
229
|
/** Augment the tool response envelope with the --explain fields and
|
|
231
230
|
* (in non-JSON mode) print the review card readably. The CLI is the
|
|
232
231
|
* single seam that places `review` + `predicateTree` on the wire
|
|
@@ -301,9 +300,11 @@ function parseConfidence(raw) {
|
|
|
301
300
|
}
|
|
302
301
|
/** Parse a strictly positive integer (windowSeconds, validUntilLedger,
|
|
303
302
|
* invocationLimit, oracleParams bounds). The core re-validates these with
|
|
304
|
-
* field-specific caps; the CLI just enforces "looks like an integer > 0".
|
|
303
|
+
* field-specific caps; the CLI just enforces "looks like an integer > 0".
|
|
304
|
+
* The `^[0-9]+$` regex already pins the shape to a non-negative integer, so
|
|
305
|
+
* the only thing left to check is "not zero". */
|
|
305
306
|
function parsePositiveInt(raw, flagName) {
|
|
306
|
-
if (!POSITIVE_INT_RE.test(raw)) {
|
|
307
|
+
if (!POSITIVE_INT_RE.test(raw) || raw === '0') {
|
|
307
308
|
throw new CliError({
|
|
308
309
|
code: 'CLI_MISSING_ARG',
|
|
309
310
|
message: `synthesize: ${flagName} "${raw}" must be a positive integer`,
|
|
@@ -311,16 +312,7 @@ function parsePositiveInt(raw, flagName) {
|
|
|
311
312
|
retryable: false,
|
|
312
313
|
});
|
|
313
314
|
}
|
|
314
|
-
|
|
315
|
-
if (!Number.isInteger(n) || n <= 0) {
|
|
316
|
-
throw new CliError({
|
|
317
|
-
code: 'CLI_MISSING_ARG',
|
|
318
|
-
message: `synthesize: ${flagName} "${raw}" must be a positive integer`,
|
|
319
|
-
severity: 'error',
|
|
320
|
-
retryable: false,
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
return n;
|
|
315
|
+
return Number(raw);
|
|
324
316
|
}
|
|
325
317
|
/** Collect ALL values for a repeatable `--<name> <value>` / `--<name>=<value>`
|
|
326
318
|
* flag from argv, in order. Unlike `parsePairs` (which keeps only the last
|
package/dist/src/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// packages/policy-builder-cli/src/index.ts - public re-exports for the CLI package.
|
|
2
2
|
export { runRecordCommand } from "./commands/record.js";
|
|
3
3
|
export { runSynthesizeCommand } from "./commands/synthesize.js";
|
|
4
4
|
export { formatToolResponse, readJsonFile, writeJsonFile } from "./output.js";
|
package/dist/src/output.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// packages/policy-builder-cli/src/output.ts
|
|
2
2
|
//
|
|
3
3
|
// Output helpers for the CLI: formatToolResponse for the `--json` flag and
|
|
4
4
|
// file I/O for `--out`. The CLI is intentionally tiny - no commander / yargs
|
|
@@ -110,20 +110,19 @@ export function writeJsonFile(path, value) {
|
|
|
110
110
|
* Throws CliError so the router can map it to a process exit code + a
|
|
111
111
|
* structured JSON envelope under --json. */
|
|
112
112
|
export function formatToolResponse(res, flags, outLabel = 'result') {
|
|
113
|
-
if (res.ok)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
return res.data;
|
|
113
|
+
if (!res.ok)
|
|
114
|
+
throw new CliError(res.error);
|
|
115
|
+
const envelope = { ok: true, data: res.data };
|
|
116
|
+
if (flags.out)
|
|
117
|
+
writeJsonFile(flags.out, envelope);
|
|
118
|
+
if (flags.json) {
|
|
119
|
+
// newline-terminated JSON so it pipes cleanly
|
|
120
|
+
process.stdout.write(`${JSON.stringify(envelope)}\n`);
|
|
121
|
+
}
|
|
122
|
+
else if (!flags.quiet) {
|
|
123
|
+
process.stdout.write(`${outLabel}: ok\n`);
|
|
125
124
|
}
|
|
126
|
-
|
|
125
|
+
return res.data;
|
|
127
126
|
}
|
|
128
127
|
/** CLI-local error class wrapping a (core or CLI) ToolError so the router can
|
|
129
128
|
* map it to a non-zero exit. The error is preserved verbatim for --json. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
//
|
|
2
|
+
// packages/policy-builder-cli/src/commands/record.ts
|
|
3
3
|
//
|
|
4
4
|
// `policy-builder record` subcommand. Thin wrapper around the core
|
|
5
5
|
// `recordTransaction` - no business logic, just argv -> RecordInput + the
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
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
5
|
// front-ends (mandate / recording) based on which file flag is supplied:
|
|
@@ -51,14 +51,7 @@ async function runSynthesizeCommand(argv, flags) {
|
|
|
51
51
|
if (hasMandate) {
|
|
52
52
|
const mandate = (0, output_ts_1.readJsonFile)(pairs.mandate);
|
|
53
53
|
const args = { source: 'mandate', mandate };
|
|
54
|
-
|
|
55
|
-
args.ozConfig = readOzConfigFile(pairs['oz-config']);
|
|
56
|
-
}
|
|
57
|
-
if (pairs.confidence !== undefined) {
|
|
58
|
-
args.confidenceOverride = { threshold: parseConfidence(pairs.confidence) };
|
|
59
|
-
}
|
|
60
|
-
if (explain)
|
|
61
|
-
args.explain = true;
|
|
54
|
+
applySharedFlags(args, pairs, explain);
|
|
62
55
|
const res = await (0, run_1.runSynthesizePolicy)(args);
|
|
63
56
|
if (explain)
|
|
64
57
|
emitExplainBlock(res, flags);
|
|
@@ -87,10 +80,7 @@ async function runSynthesizeCommand(argv, flags) {
|
|
|
87
80
|
const userResponses = {};
|
|
88
81
|
if (pairs.responses) {
|
|
89
82
|
const file = (0, output_ts_1.readJsonFile)(pairs.responses);
|
|
90
|
-
if (file
|
|
91
|
-
Object.assign(userResponses, file);
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
83
|
+
if (file === null || typeof file !== 'object' || Array.isArray(file)) {
|
|
94
84
|
throw new output_ts_1.CliError({
|
|
95
85
|
code: 'CLI_INVALID_JSON',
|
|
96
86
|
message: `synthesize: --responses ${pairs.responses} must be a JSON object`,
|
|
@@ -98,48 +88,43 @@ async function runSynthesizeCommand(argv, flags) {
|
|
|
98
88
|
retryable: false,
|
|
99
89
|
});
|
|
100
90
|
}
|
|
91
|
+
Object.assign(userResponses, file);
|
|
101
92
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
93
|
+
// Per-field overrides. Each entry: argv flag name, userResponses key,
|
|
94
|
+
// and a parser that validates the raw string.
|
|
95
|
+
const userResponseFlags = [
|
|
96
|
+
['window-seconds', 'windowSeconds', parsePositiveInt],
|
|
97
|
+
['valid-until', 'validUntilLedger', parsePositiveInt],
|
|
98
|
+
['limit-amount', 'limitAmount', parseI128String],
|
|
99
|
+
['invocation-limit', 'invocationLimit', parsePositiveInt],
|
|
100
|
+
];
|
|
101
|
+
for (const [flag, key, parse] of userResponseFlags) {
|
|
102
|
+
if (pairs[flag] !== undefined) {
|
|
103
|
+
userResponses[key] = parse(pairs[flag], `--${flag}`);
|
|
104
|
+
}
|
|
113
105
|
}
|
|
114
106
|
// --recipient <C...|G...> is REPEATABLE (parsePairs collapses duplicate keys,
|
|
115
107
|
// so it is collected straight from argv). Each value builds the swap-recipient
|
|
116
108
|
// allowlist; supplying it REPLACES the default pin to the recorded recipient.
|
|
117
109
|
const recipients = collectRepeated(argv, 'recipient');
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
});
|
|
130
|
-
}
|
|
110
|
+
for (const r of recipients) {
|
|
111
|
+
// Same validator the run-layer schema applies (SDK StrKey underneath) - a
|
|
112
|
+
// swap recipient may be a G... wallet or a C... contract. Shared rather
|
|
113
|
+
// than re-inlined so the CLI and the schema cannot drift apart.
|
|
114
|
+
if (!(0, policy_synth_1.isStellarAddress)(r)) {
|
|
115
|
+
throw new output_ts_1.CliError({
|
|
116
|
+
code: 'CLI_MISSING_ARG',
|
|
117
|
+
message: `synthesize: --recipient "${r}" is not a valid Stellar address (expected a G... wallet or C... contract)`,
|
|
118
|
+
severity: 'error',
|
|
119
|
+
retryable: false,
|
|
120
|
+
});
|
|
131
121
|
}
|
|
132
|
-
userResponses.swapRecipientAllowlist = recipients;
|
|
133
122
|
}
|
|
134
|
-
if (
|
|
123
|
+
if (recipients.length > 0)
|
|
124
|
+
userResponses.swapRecipientAllowlist = recipients;
|
|
125
|
+
if (Object.keys(userResponses).length > 0)
|
|
135
126
|
args.userResponses = userResponses;
|
|
136
|
-
|
|
137
|
-
if (pairs['oz-config'] !== undefined) {
|
|
138
|
-
args.ozConfig = readOzConfigFile(pairs['oz-config']);
|
|
139
|
-
}
|
|
140
|
-
if (pairs.confidence !== undefined) {
|
|
141
|
-
args.confidenceOverride = { threshold: parseConfidence(pairs.confidence) };
|
|
142
|
-
}
|
|
127
|
+
applySharedFlags(args, pairs, explain);
|
|
143
128
|
// --smart-account <C...> opts into the interpreter adapter, so constraints OZ
|
|
144
129
|
// cannot express (per-method scoping, invocation-count windows, oracle bounds,
|
|
145
130
|
// exact hop paths) lower to a real predicate document instead of just warnings.
|
|
@@ -149,31 +134,32 @@ async function runSynthesizeCommand(argv, flags) {
|
|
|
149
134
|
// without `--smart-account` are rejected up front instead of being silently
|
|
150
135
|
// dropped. The foot-gun: an empty value previously produced an "ok" envelope
|
|
151
136
|
// with 0 policyDocuments, so callers thought the constraint had been enforced
|
|
152
|
-
// when
|
|
137
|
+
// when it had been silently skipped.
|
|
153
138
|
const smartAccountRaw = pairs['smart-account'];
|
|
154
139
|
const installNonceRaw = pairs['install-nonce'];
|
|
155
140
|
const oracleStalenessRaw = pairs['oracle-max-staleness'];
|
|
156
141
|
const oracleDeviationRaw = pairs['oracle-max-deviation'];
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
142
|
+
// --install-nonce and the oracle knobs are interpreter-only. Reject up
|
|
143
|
+
// front so they cannot be silently dropped when --smart-account is absent.
|
|
144
|
+
if (smartAccountRaw === undefined) {
|
|
145
|
+
if (installNonceRaw !== undefined) {
|
|
146
|
+
throw new output_ts_1.CliError({
|
|
147
|
+
code: 'CLI_MISSING_ARG',
|
|
148
|
+
message: 'synthesize: --install-nonce requires --smart-account <C...> (interpreter opt-in)',
|
|
149
|
+
severity: 'error',
|
|
150
|
+
retryable: false,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
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
|
+
}
|
|
175
161
|
}
|
|
176
|
-
|
|
162
|
+
else {
|
|
177
163
|
const smartAccount = smartAccountRaw.trim();
|
|
178
164
|
if (smartAccount.length === 0) {
|
|
179
165
|
throw new output_ts_1.CliError({
|
|
@@ -230,6 +216,19 @@ async function runSynthesizeCommand(argv, flags) {
|
|
|
230
216
|
emitExplainBlock(res, flags);
|
|
231
217
|
return (0, output_ts_1.formatToolResponse)(res, flags, 'synthesize(recording)');
|
|
232
218
|
}
|
|
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. */
|
|
222
|
+
function applySharedFlags(args, pairs, explain) {
|
|
223
|
+
if (pairs['oz-config'] !== undefined) {
|
|
224
|
+
args.ozConfig = readOzConfigFile(pairs['oz-config']);
|
|
225
|
+
}
|
|
226
|
+
if (pairs.confidence !== undefined) {
|
|
227
|
+
args.confidenceOverride = { threshold: parseConfidence(pairs.confidence) };
|
|
228
|
+
}
|
|
229
|
+
if (explain)
|
|
230
|
+
args.explain = true;
|
|
231
|
+
}
|
|
233
232
|
/** Augment the tool response envelope with the --explain fields and
|
|
234
233
|
* (in non-JSON mode) print the review card readably. The CLI is the
|
|
235
234
|
* single seam that places `review` + `predicateTree` on the wire
|
|
@@ -304,9 +303,11 @@ function parseConfidence(raw) {
|
|
|
304
303
|
}
|
|
305
304
|
/** Parse a strictly positive integer (windowSeconds, validUntilLedger,
|
|
306
305
|
* invocationLimit, oracleParams bounds). The core re-validates these with
|
|
307
|
-
* field-specific caps; the CLI just enforces "looks like an integer > 0".
|
|
306
|
+
* field-specific caps; the CLI just enforces "looks like an integer > 0".
|
|
307
|
+
* The `^[0-9]+$` regex already pins the shape to a non-negative integer, so
|
|
308
|
+
* the only thing left to check is "not zero". */
|
|
308
309
|
function parsePositiveInt(raw, flagName) {
|
|
309
|
-
if (!POSITIVE_INT_RE.test(raw)) {
|
|
310
|
+
if (!POSITIVE_INT_RE.test(raw) || raw === '0') {
|
|
310
311
|
throw new output_ts_1.CliError({
|
|
311
312
|
code: 'CLI_MISSING_ARG',
|
|
312
313
|
message: `synthesize: ${flagName} "${raw}" must be a positive integer`,
|
|
@@ -314,16 +315,7 @@ function parsePositiveInt(raw, flagName) {
|
|
|
314
315
|
retryable: false,
|
|
315
316
|
});
|
|
316
317
|
}
|
|
317
|
-
|
|
318
|
-
if (!Number.isInteger(n) || n <= 0) {
|
|
319
|
-
throw new output_ts_1.CliError({
|
|
320
|
-
code: 'CLI_MISSING_ARG',
|
|
321
|
-
message: `synthesize: ${flagName} "${raw}" must be a positive integer`,
|
|
322
|
-
severity: 'error',
|
|
323
|
-
retryable: false,
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
return n;
|
|
318
|
+
return Number(raw);
|
|
327
319
|
}
|
|
328
320
|
/** Collect ALL values for a repeatable `--<name> <value>` / `--<name>=<value>`
|
|
329
321
|
* flag from argv, in order. Unlike `parsePairs` (which keeps only the last
|
package/dist-cjs/src/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
//
|
|
2
|
+
// packages/policy-builder-cli/src/index.ts - public re-exports for the CLI package.
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.writeJsonFile = exports.readJsonFile = exports.formatToolResponse = exports.runSynthesizeCommand = exports.runRecordCommand = void 0;
|
|
5
5
|
var record_ts_1 = require("./commands/record.js");
|
package/dist-cjs/src/output.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
//
|
|
2
|
+
// packages/policy-builder-cli/src/output.ts
|
|
3
3
|
//
|
|
4
4
|
// Output helpers for the CLI: formatToolResponse for the `--json` flag and
|
|
5
5
|
// file I/O for `--out`. The CLI is intentionally tiny - no commander / yargs
|
|
@@ -119,20 +119,19 @@ function writeJsonFile(path, value) {
|
|
|
119
119
|
* Throws CliError so the router can map it to a process exit code + a
|
|
120
120
|
* structured JSON envelope under --json. */
|
|
121
121
|
function formatToolResponse(res, flags, outLabel = 'result') {
|
|
122
|
-
if (res.ok)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
return res.data;
|
|
122
|
+
if (!res.ok)
|
|
123
|
+
throw new CliError(res.error);
|
|
124
|
+
const envelope = { ok: true, data: res.data };
|
|
125
|
+
if (flags.out)
|
|
126
|
+
writeJsonFile(flags.out, envelope);
|
|
127
|
+
if (flags.json) {
|
|
128
|
+
// newline-terminated JSON so it pipes cleanly
|
|
129
|
+
process.stdout.write(`${JSON.stringify(envelope)}\n`);
|
|
130
|
+
}
|
|
131
|
+
else if (!flags.quiet) {
|
|
132
|
+
process.stdout.write(`${outLabel}: ok\n`);
|
|
134
133
|
}
|
|
135
|
-
|
|
134
|
+
return res.data;
|
|
136
135
|
}
|
|
137
136
|
/** CLI-local error class wrapping a (core or CLI) ToolError so the router can
|
|
138
137
|
* map it to a non-zero exit. The error is preserved verbatim for --json. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crediolabs/policy-builder-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
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/
|
|
16
|
-
"directory": "
|
|
15
|
+
"url": "https://github.com/untangledfinance/octogate.git",
|
|
16
|
+
"directory": "packages/policy-builder-cli"
|
|
17
17
|
},
|
|
18
|
-
"homepage": "https://github.com/untangledfinance/
|
|
18
|
+
"homepage": "https://github.com/untangledfinance/octogate#readme",
|
|
19
19
|
"bugs": {
|
|
20
|
-
"url": "https://github.com/untangledfinance/
|
|
20
|
+
"url": "https://github.com/untangledfinance/octogate/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.
|
|
66
|
+
"@crediolabs/policy-synth": "0.1.18",
|
|
67
67
|
"@stellar/stellar-sdk": "14.4.0",
|
|
68
68
|
"zod": "3.25.76"
|
|
69
69
|
},
|
package/src/commands/record.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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
4
|
// front-ends (mandate / recording) based on which file flag is supplied:
|
|
@@ -72,13 +72,7 @@ export async function runSynthesizeCommand(
|
|
|
72
72
|
if (hasMandate) {
|
|
73
73
|
const mandate = readJsonFile(pairs.mandate as string) as Record<string, unknown>
|
|
74
74
|
const args: Record<string, unknown> = { source: 'mandate', mandate }
|
|
75
|
-
|
|
76
|
-
args.ozConfig = readOzConfigFile(pairs['oz-config'] as string)
|
|
77
|
-
}
|
|
78
|
-
if (pairs.confidence !== undefined) {
|
|
79
|
-
args.confidenceOverride = { threshold: parseConfidence(pairs.confidence as string) }
|
|
80
|
-
}
|
|
81
|
-
if (explain) args.explain = true
|
|
75
|
+
applySharedFlags(args, pairs, explain)
|
|
82
76
|
const res = await runSynthesizePolicy(args)
|
|
83
77
|
if (explain) emitExplainBlock(res, flags)
|
|
84
78
|
return formatToolResponse(res, flags, 'synthesize(mandate)')
|
|
@@ -109,9 +103,7 @@ export async function runSynthesizeCommand(
|
|
|
109
103
|
const userResponses: Record<string, unknown> = {}
|
|
110
104
|
if (pairs.responses) {
|
|
111
105
|
const file = readJsonFile(pairs.responses)
|
|
112
|
-
if (file
|
|
113
|
-
Object.assign(userResponses, file as Record<string, unknown>)
|
|
114
|
-
} else {
|
|
106
|
+
if (file === null || typeof file !== 'object' || Array.isArray(file)) {
|
|
115
107
|
throw new CliError({
|
|
116
108
|
code: 'CLI_INVALID_JSON',
|
|
117
109
|
message: `synthesize: --responses ${pairs.responses} must be a JSON object`,
|
|
@@ -119,58 +111,42 @@ export async function runSynthesizeCommand(
|
|
|
119
111
|
retryable: false,
|
|
120
112
|
})
|
|
121
113
|
}
|
|
114
|
+
Object.assign(userResponses, file as Record<string, unknown>)
|
|
122
115
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (pairs['limit-amount'] !== undefined) {
|
|
136
|
-
userResponses.limitAmount = parseI128String(pairs['limit-amount'] as string, '--limit-amount')
|
|
137
|
-
}
|
|
138
|
-
if (pairs['invocation-limit'] !== undefined) {
|
|
139
|
-
userResponses.invocationLimit = parsePositiveInt(
|
|
140
|
-
pairs['invocation-limit'] as string,
|
|
141
|
-
'--invocation-limit'
|
|
142
|
-
)
|
|
116
|
+
// Per-field overrides. Each entry: argv flag name, userResponses key,
|
|
117
|
+
// and a parser that validates the raw string.
|
|
118
|
+
const userResponseFlags: Array<[string, string, (raw: string, flag: string) => unknown]> = [
|
|
119
|
+
['window-seconds', 'windowSeconds', parsePositiveInt],
|
|
120
|
+
['valid-until', 'validUntilLedger', parsePositiveInt],
|
|
121
|
+
['limit-amount', 'limitAmount', parseI128String],
|
|
122
|
+
['invocation-limit', 'invocationLimit', parsePositiveInt],
|
|
123
|
+
]
|
|
124
|
+
for (const [flag, key, parse] of userResponseFlags) {
|
|
125
|
+
if (pairs[flag] !== undefined) {
|
|
126
|
+
userResponses[key] = parse(pairs[flag] as string, `--${flag}`)
|
|
127
|
+
}
|
|
143
128
|
}
|
|
144
129
|
// --recipient <C...|G...> is REPEATABLE (parsePairs collapses duplicate keys,
|
|
145
130
|
// so it is collected straight from argv). Each value builds the swap-recipient
|
|
146
131
|
// allowlist; supplying it REPLACES the default pin to the recorded recipient.
|
|
147
132
|
const recipients = collectRepeated(argv, 'recipient')
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
})
|
|
160
|
-
}
|
|
133
|
+
for (const r of recipients) {
|
|
134
|
+
// Same validator the run-layer schema applies (SDK StrKey underneath) - a
|
|
135
|
+
// swap recipient may be a G... wallet or a C... contract. Shared rather
|
|
136
|
+
// than re-inlined so the CLI and the schema cannot drift apart.
|
|
137
|
+
if (!isStellarAddress(r)) {
|
|
138
|
+
throw new CliError({
|
|
139
|
+
code: 'CLI_MISSING_ARG',
|
|
140
|
+
message: `synthesize: --recipient "${r}" is not a valid Stellar address (expected a G... wallet or C... contract)`,
|
|
141
|
+
severity: 'error',
|
|
142
|
+
retryable: false,
|
|
143
|
+
})
|
|
161
144
|
}
|
|
162
|
-
userResponses.swapRecipientAllowlist = recipients
|
|
163
|
-
}
|
|
164
|
-
if (Object.keys(userResponses).length > 0) {
|
|
165
|
-
args.userResponses = userResponses
|
|
166
145
|
}
|
|
146
|
+
if (recipients.length > 0) userResponses.swapRecipientAllowlist = recipients
|
|
147
|
+
if (Object.keys(userResponses).length > 0) args.userResponses = userResponses
|
|
167
148
|
|
|
168
|
-
|
|
169
|
-
args.ozConfig = readOzConfigFile(pairs['oz-config'] as string)
|
|
170
|
-
}
|
|
171
|
-
if (pairs.confidence !== undefined) {
|
|
172
|
-
args.confidenceOverride = { threshold: parseConfidence(pairs.confidence as string) }
|
|
173
|
-
}
|
|
149
|
+
applySharedFlags(args, pairs, explain)
|
|
174
150
|
|
|
175
151
|
// --smart-account <C...> opts into the interpreter adapter, so constraints OZ
|
|
176
152
|
// cannot express (per-method scoping, invocation-count windows, oracle bounds,
|
|
@@ -181,34 +157,32 @@ export async function runSynthesizeCommand(
|
|
|
181
157
|
// without `--smart-account` are rejected up front instead of being silently
|
|
182
158
|
// dropped. The foot-gun: an empty value previously produced an "ok" envelope
|
|
183
159
|
// with 0 policyDocuments, so callers thought the constraint had been enforced
|
|
184
|
-
// when
|
|
160
|
+
// when it had been silently skipped.
|
|
185
161
|
const smartAccountRaw = pairs['smart-account']
|
|
186
162
|
const installNonceRaw = pairs['install-nonce']
|
|
187
163
|
const oracleStalenessRaw = pairs['oracle-max-staleness']
|
|
188
164
|
const oracleDeviationRaw = pairs['oracle-max-deviation']
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
(oracleStalenessRaw !== undefined || oracleDeviationRaw !== undefined)
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
if (smartAccountRaw !== undefined) {
|
|
165
|
+
// --install-nonce and the oracle knobs are interpreter-only. Reject up
|
|
166
|
+
// front so they cannot be silently dropped when --smart-account is absent.
|
|
167
|
+
if (smartAccountRaw === undefined) {
|
|
168
|
+
if (installNonceRaw !== undefined) {
|
|
169
|
+
throw new CliError({
|
|
170
|
+
code: 'CLI_MISSING_ARG',
|
|
171
|
+
message: 'synthesize: --install-nonce requires --smart-account <C...> (interpreter opt-in)',
|
|
172
|
+
severity: 'error',
|
|
173
|
+
retryable: false,
|
|
174
|
+
})
|
|
175
|
+
}
|
|
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
|
+
} else {
|
|
212
186
|
const smartAccount = smartAccountRaw.trim()
|
|
213
187
|
if (smartAccount.length === 0) {
|
|
214
188
|
throw new CliError({
|
|
@@ -271,6 +245,23 @@ export async function runSynthesizeCommand(
|
|
|
271
245
|
return formatToolResponse(res, flags, 'synthesize(recording)')
|
|
272
246
|
}
|
|
273
247
|
|
|
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. */
|
|
251
|
+
function applySharedFlags(
|
|
252
|
+
args: Record<string, unknown>,
|
|
253
|
+
pairs: Record<string, string>,
|
|
254
|
+
explain: boolean
|
|
255
|
+
): void {
|
|
256
|
+
if (pairs['oz-config'] !== undefined) {
|
|
257
|
+
args.ozConfig = readOzConfigFile(pairs['oz-config'] as string)
|
|
258
|
+
}
|
|
259
|
+
if (pairs.confidence !== undefined) {
|
|
260
|
+
args.confidenceOverride = { threshold: parseConfidence(pairs.confidence as string) }
|
|
261
|
+
}
|
|
262
|
+
if (explain) args.explain = true
|
|
263
|
+
}
|
|
264
|
+
|
|
274
265
|
/** Augment the tool response envelope with the --explain fields and
|
|
275
266
|
* (in non-JSON mode) print the review card readably. The CLI is the
|
|
276
267
|
* single seam that places `review` + `predicateTree` on the wire
|
|
@@ -366,9 +357,11 @@ function parseConfidence(raw: string): number {
|
|
|
366
357
|
|
|
367
358
|
/** Parse a strictly positive integer (windowSeconds, validUntilLedger,
|
|
368
359
|
* invocationLimit, oracleParams bounds). The core re-validates these with
|
|
369
|
-
* field-specific caps; the CLI just enforces "looks like an integer > 0".
|
|
360
|
+
* field-specific caps; the CLI just enforces "looks like an integer > 0".
|
|
361
|
+
* The `^[0-9]+$` regex already pins the shape to a non-negative integer, so
|
|
362
|
+
* the only thing left to check is "not zero". */
|
|
370
363
|
function parsePositiveInt(raw: string, flagName: string): number {
|
|
371
|
-
if (!POSITIVE_INT_RE.test(raw)) {
|
|
364
|
+
if (!POSITIVE_INT_RE.test(raw) || raw === '0') {
|
|
372
365
|
throw new CliError({
|
|
373
366
|
code: 'CLI_MISSING_ARG',
|
|
374
367
|
message: `synthesize: ${flagName} "${raw}" must be a positive integer`,
|
|
@@ -376,16 +369,7 @@ function parsePositiveInt(raw: string, flagName: string): number {
|
|
|
376
369
|
retryable: false,
|
|
377
370
|
})
|
|
378
371
|
}
|
|
379
|
-
|
|
380
|
-
if (!Number.isInteger(n) || n <= 0) {
|
|
381
|
-
throw new CliError({
|
|
382
|
-
code: 'CLI_MISSING_ARG',
|
|
383
|
-
message: `synthesize: ${flagName} "${raw}" must be a positive integer`,
|
|
384
|
-
severity: 'error',
|
|
385
|
-
retryable: false,
|
|
386
|
-
})
|
|
387
|
-
}
|
|
388
|
-
return n
|
|
372
|
+
return Number(raw)
|
|
389
373
|
}
|
|
390
374
|
|
|
391
375
|
/** Collect ALL values for a repeatable `--<name> <value>` / `--<name>=<value>`
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// packages/policy-builder-cli/src/index.ts - public re-exports for the CLI package.
|
|
2
2
|
|
|
3
3
|
export { runRecordCommand } from './commands/record.ts'
|
|
4
4
|
export { runSynthesizeCommand } from './commands/synthesize.ts'
|
package/src/output.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// packages/policy-builder-cli/src/output.ts
|
|
2
2
|
//
|
|
3
3
|
// Output helpers for the CLI: formatToolResponse for the `--json` flag and
|
|
4
4
|
// file I/O for `--out`. The CLI is intentionally tiny - no commander / yargs
|
|
@@ -120,18 +120,16 @@ export function formatToolResponse<T>(
|
|
|
120
120
|
flags: CliFlags,
|
|
121
121
|
outLabel = 'result'
|
|
122
122
|
): T {
|
|
123
|
-
if (res.ok)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
return res.data
|
|
123
|
+
if (!res.ok) throw new CliError(res.error)
|
|
124
|
+
const envelope = { ok: true as const, data: res.data }
|
|
125
|
+
if (flags.out) writeJsonFile(flags.out, envelope)
|
|
126
|
+
if (flags.json) {
|
|
127
|
+
// newline-terminated JSON so it pipes cleanly
|
|
128
|
+
process.stdout.write(`${JSON.stringify(envelope)}\n`)
|
|
129
|
+
} else if (!flags.quiet) {
|
|
130
|
+
process.stdout.write(`${outLabel}: ok\n`)
|
|
133
131
|
}
|
|
134
|
-
|
|
132
|
+
return res.data
|
|
135
133
|
}
|
|
136
134
|
|
|
137
135
|
/** CLI-specific error codes, distinct from the core's ErrorCode union. They
|