@crediolabs/policy-builder-cli 0.1.5 → 0.1.7
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/LICENSE +21 -0
- package/README.md +60 -2
- package/dist/bin/policy-builder.js +42 -8
- package/dist/src/commands/record.js +1 -1
- package/dist/src/commands/synthesize.js +194 -7
- package/dist/src/output.d.ts +8 -1
- package/dist/src/output.js +24 -5
- package/dist-cjs/package.json +3 -0
- package/dist-cjs/src/commands/record.d.ts +3 -0
- package/dist-cjs/src/commands/record.js +45 -0
- package/dist-cjs/src/commands/synthesize.d.ts +3 -0
- package/dist-cjs/src/commands/synthesize.js +253 -0
- package/dist-cjs/src/index.d.ts +3 -0
- package/dist-cjs/src/index.js +12 -0
- package/dist-cjs/src/output.d.ts +56 -0
- package/dist-cjs/src/output.js +166 -0
- package/package.json +36 -4
- package/src/commands/record.ts +1 -1
- package/src/commands/synthesize.ts +220 -7
- package/src/output.ts +22 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Untangled Finance Limited
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -19,10 +19,16 @@ policy-builder record --network <mainnet|testnet> --hash <tx> | --xdr <b64>
|
|
|
19
19
|
[--json] [--quiet] [--out <path>]
|
|
20
20
|
|
|
21
21
|
policy-builder synthesize --mandate <path.json>
|
|
22
|
+
[--oz-config <path.json>] [--confidence <0..1>]
|
|
22
23
|
[--json] [--quiet] [--out <path>]
|
|
23
24
|
|
|
24
25
|
policy-builder synthesize --recorded-tx <path.json> --network <mainnet|testnet>
|
|
25
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>]
|
|
26
32
|
[--json] [--quiet] [--out <path>]
|
|
27
33
|
```
|
|
28
34
|
|
|
@@ -31,8 +37,47 @@ Global flags:
|
|
|
31
37
|
- `--json` — emit a machine-readable JSON envelope (`{ ok, data }` / `{ ok, error }`) on stdout.
|
|
32
38
|
- `--quiet` — suppress progress / non-error output.
|
|
33
39
|
- `--out <path>` — write the artefact to a file (JSON).
|
|
34
|
-
|
|
35
|
-
|
|
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.
|
|
36
81
|
|
|
37
82
|
## Examples
|
|
38
83
|
|
|
@@ -43,6 +88,19 @@ policy-builder record --network mainnet --hash <tx-hash> --out recorded-tx.json
|
|
|
43
88
|
policy-builder synthesize --recorded-tx recorded-tx.json --network mainnet --json
|
|
44
89
|
```
|
|
45
90
|
|
|
91
|
+
Synthesise with the interpreter opt-in (use when the policy needs constraints
|
|
92
|
+
OZ built-ins cannot express — e.g. per-claim frequency caps on Blend):
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
policy-builder synthesize \
|
|
96
|
+
--recorded-tx recorded-tx.json \
|
|
97
|
+
--network mainnet \
|
|
98
|
+
--responses responses.json \
|
|
99
|
+
--smart-account CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIT4C \
|
|
100
|
+
--install-nonce 1 \
|
|
101
|
+
--json
|
|
102
|
+
```
|
|
103
|
+
|
|
46
104
|
Synthesise deterministically from a mandate (`mandate.json`):
|
|
47
105
|
|
|
48
106
|
```json
|
|
@@ -5,10 +5,15 @@
|
|
|
5
5
|
// policy-builder record --network <mainnet|testnet> --hash <tx>
|
|
6
6
|
// [--xdr <b64>] [--json] [--quiet] [--out <path>]
|
|
7
7
|
// policy-builder synthesize --mandate <path.json>
|
|
8
|
+
// [--oz-config <path.json>] [--confidence <0..1>]
|
|
8
9
|
// [--json] [--quiet] [--out <path>]
|
|
9
10
|
// policy-builder synthesize --recorded-tx <path.json> --network <mainnet|testnet>
|
|
10
11
|
// [--responses <path.json>]
|
|
12
|
+
// [--oz-config <path.json>] [--confidence <0..1>]
|
|
11
13
|
// [--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>]
|
|
12
17
|
// [--json] [--quiet] [--out <path>]
|
|
13
18
|
//
|
|
14
19
|
// The router is hand-rolled (no commander / yargs dep). It splits argv into
|
|
@@ -59,21 +64,50 @@ Usage:
|
|
|
59
64
|
policy-builder record --network <mainnet|testnet> --hash <tx> | --xdr <b64>
|
|
60
65
|
[--json] [--quiet] [--out <path>]
|
|
61
66
|
policy-builder synthesize --mandate <path.json>
|
|
67
|
+
[--oz-config <path.json>] [--confidence <0..1>]
|
|
62
68
|
[--json] [--quiet] [--out <path>]
|
|
63
69
|
policy-builder synthesize --recorded-tx <path.json> --network <mainnet|testnet>
|
|
64
70
|
[--responses <path.json>]
|
|
71
|
+
[--oz-config <path.json>] [--confidence <0..1>]
|
|
65
72
|
[--smart-account <C...>] [--install-nonce <n>]
|
|
73
|
+
[--oracle-max-staleness <s>] [--oracle-max-deviation <bps>]
|
|
74
|
+
[--window-seconds <n>] [--valid-until <ledger>]
|
|
75
|
+
[--limit-amount <i128str>] [--invocation-limit <n>]
|
|
66
76
|
[--json] [--quiet] [--out <path>]
|
|
67
77
|
|
|
68
78
|
Flags:
|
|
69
|
-
--json
|
|
70
|
-
--quiet
|
|
71
|
-
--out
|
|
72
|
-
--
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
79
|
+
--json emit machine-readable JSON on stdout
|
|
80
|
+
--quiet suppress progress / non-error output
|
|
81
|
+
--out write artefact to file (JSON)
|
|
82
|
+
--oz-config path to a JSON OzAdapterConfig (custom OZ instance
|
|
83
|
+
addresses for the network); applies to both the
|
|
84
|
+
--mandate and --recorded-tx synthesize paths
|
|
85
|
+
--confidence recorder confidence GATE threshold (0..1, inclusive);
|
|
86
|
+
rejects when parseConfidence.overall < threshold; does
|
|
87
|
+
not change the recording's parseConfidence.thresholdUsed
|
|
88
|
+
in the output
|
|
89
|
+
--smart-account C... account to opt into the interpreter adapter, so
|
|
90
|
+
non-OZ constraints (per-method scoping, invocation-count
|
|
91
|
+
windows, oracle bounds, exact hop paths) lower to a real
|
|
92
|
+
predicate document instead of warnings
|
|
93
|
+
--install-nonce per-rule install nonce for the interpreter policy
|
|
94
|
+
(default 1); requires --smart-account
|
|
95
|
+
--oracle-max-staleness interpreter oracle max staleness in seconds
|
|
96
|
+
(positive integer <= 600, tighten-only);
|
|
97
|
+
requires --smart-account
|
|
98
|
+
--oracle-max-deviation interpreter oracle max deviation in basis points
|
|
99
|
+
(positive integer <= 200, tighten-only);
|
|
100
|
+
requires --smart-account
|
|
101
|
+
--window-seconds per-field override of userResponses.windowSeconds;
|
|
102
|
+
overrides the same field from --responses (CLI flag
|
|
103
|
+
wins)
|
|
104
|
+
--valid-until per-field override of userResponses.validUntilLedger;
|
|
105
|
+
overrides the same field from --responses
|
|
106
|
+
--limit-amount per-field override of userResponses.limitAmount
|
|
107
|
+
(i128 decimal string); overrides the same field
|
|
108
|
+
from --responses
|
|
109
|
+
--invocation-limit per-field override of userResponses.invocationLimit;
|
|
110
|
+
overrides the same field from --responses
|
|
77
111
|
`);
|
|
78
112
|
}
|
|
79
113
|
await main();
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// `policy-builder record` subcommand. Thin wrapper around the core
|
|
4
4
|
// `recordTransaction` - no business logic, just argv -> RecordInput + the
|
|
5
5
|
// canonical CLI envelope.
|
|
6
|
-
import { runRecordTransaction } from '@crediolabs/policy-
|
|
6
|
+
import { runRecordTransaction } from '@crediolabs/policy-synth/run';
|
|
7
7
|
import { CliError, formatToolResponse, parsePairs } from "../output.js";
|
|
8
8
|
export async function runRecordCommand(argv, flags) {
|
|
9
9
|
const pairs = parsePairs(argv);
|
|
@@ -7,8 +7,19 @@
|
|
|
7
7
|
//
|
|
8
8
|
// The CLI mirrors the MCP tool's discriminated union: one subcommand, two
|
|
9
9
|
// front-ends, mutually exclusive.
|
|
10
|
-
|
|
10
|
+
//
|
|
11
|
+
// Per-field response flags (--window-seconds, --valid-until, --limit-amount,
|
|
12
|
+
// --invocation-limit) merge into `userResponses`. A flag overrides the same
|
|
13
|
+
// 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
|
+
// the interpreter opt-in and are rejected without --smart-account; tighten-only
|
|
16
|
+
// bounds are validated by the core.
|
|
17
|
+
import { runSynthesizePolicy } from '@crediolabs/policy-synth/run';
|
|
11
18
|
import { CliError, formatToolResponse, parsePairs, readJsonFile } from "../output.js";
|
|
19
|
+
// Positive-int flags and i128 amount strings share the same wire shape: a
|
|
20
|
+
// base-10 unsigned decimal, no sign. The i128 stays a string at the boundary
|
|
21
|
+
// because it is wider than Number.MAX_SAFE_INTEGER.
|
|
22
|
+
const POSITIVE_INT_RE = /^[0-9]+$/;
|
|
12
23
|
export async function runSynthesizeCommand(argv, flags) {
|
|
13
24
|
const pairs = parsePairs(argv);
|
|
14
25
|
const hasMandate = Boolean(pairs.mandate);
|
|
@@ -23,7 +34,14 @@ export async function runSynthesizeCommand(argv, flags) {
|
|
|
23
34
|
}
|
|
24
35
|
if (hasMandate) {
|
|
25
36
|
const mandate = readJsonFile(pairs.mandate);
|
|
26
|
-
const
|
|
37
|
+
const args = { source: 'mandate', mandate };
|
|
38
|
+
if (pairs['oz-config'] !== undefined) {
|
|
39
|
+
args.ozConfig = readOzConfigFile(pairs['oz-config']);
|
|
40
|
+
}
|
|
41
|
+
if (pairs.confidence !== undefined) {
|
|
42
|
+
args.confidenceOverride = { threshold: parseConfidence(pairs.confidence) };
|
|
43
|
+
}
|
|
44
|
+
const res = await runSynthesizePolicy(args);
|
|
27
45
|
return formatToolResponse(res, flags, 'synthesize(mandate)');
|
|
28
46
|
}
|
|
29
47
|
// hasRecorded
|
|
@@ -44,20 +62,189 @@ export async function runSynthesizeCommand(argv, flags) {
|
|
|
44
62
|
});
|
|
45
63
|
}
|
|
46
64
|
const args = { source: 'recording', recordedTx, network };
|
|
65
|
+
// userResponses precedence: --responses file is the base; per-field flags
|
|
66
|
+
// override the same field. Only the override'd fields are merged in.
|
|
67
|
+
const userResponses = {};
|
|
47
68
|
if (pairs.responses) {
|
|
48
|
-
|
|
69
|
+
const file = readJsonFile(pairs.responses);
|
|
70
|
+
if (file !== null && typeof file === 'object' && !Array.isArray(file)) {
|
|
71
|
+
Object.assign(userResponses, file);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
throw new CliError({
|
|
75
|
+
code: 'CLI_INVALID_JSON',
|
|
76
|
+
message: `synthesize: --responses ${pairs.responses} must be a JSON object`,
|
|
77
|
+
severity: 'error',
|
|
78
|
+
retryable: false,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (pairs['window-seconds'] !== undefined) {
|
|
83
|
+
userResponses.windowSeconds = parsePositiveInt(pairs['window-seconds'], '--window-seconds');
|
|
84
|
+
}
|
|
85
|
+
if (pairs['valid-until'] !== undefined) {
|
|
86
|
+
userResponses.validUntilLedger = parsePositiveInt(pairs['valid-until'], '--valid-until');
|
|
87
|
+
}
|
|
88
|
+
if (pairs['limit-amount'] !== undefined) {
|
|
89
|
+
userResponses.limitAmount = parseI128String(pairs['limit-amount'], '--limit-amount');
|
|
90
|
+
}
|
|
91
|
+
if (pairs['invocation-limit'] !== undefined) {
|
|
92
|
+
userResponses.invocationLimit = parsePositiveInt(pairs['invocation-limit'], '--invocation-limit');
|
|
93
|
+
}
|
|
94
|
+
if (Object.keys(userResponses).length > 0) {
|
|
95
|
+
args.userResponses = userResponses;
|
|
96
|
+
}
|
|
97
|
+
if (pairs['oz-config'] !== undefined) {
|
|
98
|
+
args.ozConfig = readOzConfigFile(pairs['oz-config']);
|
|
99
|
+
}
|
|
100
|
+
if (pairs.confidence !== undefined) {
|
|
101
|
+
args.confidenceOverride = { threshold: parseConfidence(pairs.confidence) };
|
|
49
102
|
}
|
|
50
103
|
// --smart-account <C...> opts into the interpreter adapter, so constraints OZ
|
|
51
104
|
// cannot express (per-method scoping, invocation-count windows, oracle bounds,
|
|
52
105
|
// exact hop paths) lower to a real predicate document instead of just warnings.
|
|
53
106
|
// The core validates the address and installNonce; a bad value surfaces there.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
107
|
+
//
|
|
108
|
+
// Use `!== undefined` (not truthy) so `--smart-account ""` and `--install-nonce`
|
|
109
|
+
// without `--smart-account` are rejected up front instead of being silently
|
|
110
|
+
// dropped. The foot-gun: an empty value previously produced an "ok" envelope
|
|
111
|
+
// with 0 policyDocuments, so callers thought the constraint had been enforced
|
|
112
|
+
// when in fact it had been silently skipped.
|
|
113
|
+
const smartAccountRaw = pairs['smart-account'];
|
|
114
|
+
const installNonceRaw = pairs['install-nonce'];
|
|
115
|
+
const oracleStalenessRaw = pairs['oracle-max-staleness'];
|
|
116
|
+
const oracleDeviationRaw = pairs['oracle-max-deviation'];
|
|
117
|
+
if (installNonceRaw !== undefined && smartAccountRaw === undefined) {
|
|
118
|
+
throw new CliError({
|
|
119
|
+
code: 'CLI_MISSING_ARG',
|
|
120
|
+
message: 'synthesize: --install-nonce requires --smart-account <C...> (interpreter opt-in)',
|
|
121
|
+
severity: 'error',
|
|
122
|
+
retryable: false,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
// Oracle params are an interpreter-only knob; reject up front so they cannot
|
|
126
|
+
// be silently dropped when --smart-account is absent.
|
|
127
|
+
if ((oracleStalenessRaw !== undefined || oracleDeviationRaw !== undefined) &&
|
|
128
|
+
smartAccountRaw === undefined) {
|
|
129
|
+
throw new CliError({
|
|
130
|
+
code: 'CLI_MISSING_ARG',
|
|
131
|
+
message: 'synthesize: --oracle-max-staleness / --oracle-max-deviation require --smart-account <C...> (interpreter opt-in)',
|
|
132
|
+
severity: 'error',
|
|
133
|
+
retryable: false,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (smartAccountRaw !== undefined) {
|
|
137
|
+
const smartAccount = smartAccountRaw.trim();
|
|
138
|
+
if (smartAccount.length === 0) {
|
|
139
|
+
throw new CliError({
|
|
140
|
+
code: 'CLI_MISSING_ARG',
|
|
141
|
+
message: 'synthesize: --smart-account <C...> was passed empty; provide a 56-character contract strkey or omit the flag',
|
|
142
|
+
severity: 'error',
|
|
143
|
+
retryable: false,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (!/^C[2-7A-Z]{55}$/.test(smartAccount)) {
|
|
147
|
+
throw new CliError({
|
|
148
|
+
code: 'CLI_MISSING_ARG',
|
|
149
|
+
message: `synthesize: --smart-account "${smartAccount}" is not a valid C... contract strkey (expected 56 chars starting with C)`,
|
|
150
|
+
severity: 'error',
|
|
151
|
+
retryable: false,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
const interpreter = { smartAccountAddress: smartAccount };
|
|
155
|
+
if (installNonceRaw !== undefined) {
|
|
156
|
+
const nonce = Number(installNonceRaw);
|
|
157
|
+
if (!Number.isInteger(nonce) || nonce < 0) {
|
|
158
|
+
throw new CliError({
|
|
159
|
+
code: 'CLI_MISSING_ARG',
|
|
160
|
+
message: `synthesize: --install-nonce "${installNonceRaw}" is not a non-negative integer`,
|
|
161
|
+
severity: 'error',
|
|
162
|
+
retryable: false,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
interpreter.installNonce = nonce;
|
|
166
|
+
}
|
|
167
|
+
// Oracle params only attach when at least one bound was provided. The
|
|
168
|
+
// core validates tighten-only (maxStalenessSeconds <= 600,
|
|
169
|
+
// maxDeviationBps <= 200) - a too-loose value surfaces as SYNTHESIS_ERROR.
|
|
170
|
+
if (oracleStalenessRaw !== undefined || oracleDeviationRaw !== undefined) {
|
|
171
|
+
const oracleParams = {};
|
|
172
|
+
if (oracleStalenessRaw !== undefined) {
|
|
173
|
+
oracleParams.maxStalenessSeconds = parsePositiveInt(oracleStalenessRaw, '--oracle-max-staleness');
|
|
174
|
+
}
|
|
175
|
+
if (oracleDeviationRaw !== undefined) {
|
|
176
|
+
oracleParams.maxDeviationBps = parsePositiveInt(oracleDeviationRaw, '--oracle-max-deviation');
|
|
177
|
+
}
|
|
178
|
+
interpreter.oracleParams = oracleParams;
|
|
58
179
|
}
|
|
59
180
|
args.interpreter = interpreter;
|
|
60
181
|
}
|
|
61
182
|
const res = await runSynthesizePolicy(args);
|
|
62
183
|
return formatToolResponse(res, flags, 'synthesize(recording)');
|
|
63
184
|
}
|
|
185
|
+
/** Read and validate an OzAdapterConfig JSON file. Throws CLI_FILE_NOT_FOUND /
|
|
186
|
+
* CLI_INVALID_JSON for filesystem / parse failures; the core's strict schema
|
|
187
|
+
* on `ozConfig` catches shape mismatches downstream. */
|
|
188
|
+
function readOzConfigFile(path) {
|
|
189
|
+
const value = readJsonFile(path);
|
|
190
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
191
|
+
throw new CliError({
|
|
192
|
+
code: 'CLI_INVALID_JSON',
|
|
193
|
+
message: `synthesize: --oz-config ${path} must be a JSON object`,
|
|
194
|
+
severity: 'error',
|
|
195
|
+
retryable: false,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return value;
|
|
199
|
+
}
|
|
200
|
+
/** Parse and validate `--confidence <n>` as a finite number in [0, 1]. A
|
|
201
|
+
* threshold above 1 would disable the recorder gate; reject it up front. */
|
|
202
|
+
function parseConfidence(raw) {
|
|
203
|
+
const n = Number(raw);
|
|
204
|
+
if (!Number.isFinite(n) || n < 0 || n > 1) {
|
|
205
|
+
throw new CliError({
|
|
206
|
+
code: 'CLI_MISSING_ARG',
|
|
207
|
+
message: `synthesize: --confidence "${raw}" must be a finite number within [0, 1]`,
|
|
208
|
+
severity: 'error',
|
|
209
|
+
retryable: false,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return n;
|
|
213
|
+
}
|
|
214
|
+
/** Parse a strictly positive integer (windowSeconds, validUntilLedger,
|
|
215
|
+
* invocationLimit, oracleParams bounds). The core re-validates these with
|
|
216
|
+
* field-specific caps; the CLI just enforces "looks like an integer > 0". */
|
|
217
|
+
function parsePositiveInt(raw, flagName) {
|
|
218
|
+
if (!POSITIVE_INT_RE.test(raw)) {
|
|
219
|
+
throw new CliError({
|
|
220
|
+
code: 'CLI_MISSING_ARG',
|
|
221
|
+
message: `synthesize: ${flagName} "${raw}" must be a positive integer`,
|
|
222
|
+
severity: 'error',
|
|
223
|
+
retryable: false,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
const n = Number(raw);
|
|
227
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
228
|
+
throw new CliError({
|
|
229
|
+
code: 'CLI_MISSING_ARG',
|
|
230
|
+
message: `synthesize: ${flagName} "${raw}" must be a positive integer`,
|
|
231
|
+
severity: 'error',
|
|
232
|
+
retryable: false,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
return n;
|
|
236
|
+
}
|
|
237
|
+
/** Parse an i128 decimal string (positive, base 10). The synth gate, not the
|
|
238
|
+
* CLI, decides what to do with negatives - real recordings carry positive
|
|
239
|
+
* amounts on the wire for `limitAmount`. */
|
|
240
|
+
function parseI128String(raw, flagName) {
|
|
241
|
+
if (!POSITIVE_INT_RE.test(raw)) {
|
|
242
|
+
throw new CliError({
|
|
243
|
+
code: 'CLI_MISSING_ARG',
|
|
244
|
+
message: `synthesize: ${flagName} "${raw}" must be a positive decimal integer string (base-10 i128)`,
|
|
245
|
+
severity: 'error',
|
|
246
|
+
retryable: false,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return raw;
|
|
250
|
+
}
|
package/dist/src/output.d.ts
CHANGED
|
@@ -10,7 +10,14 @@ export interface CliFlags {
|
|
|
10
10
|
export declare function parseFlags(argv: ReadonlyArray<string>): CliFlags;
|
|
11
11
|
/** Resolve `--value <v>` style pairs after the subcommand name. Returns
|
|
12
12
|
* an object keyed by the option name (without `--`). Throws on missing
|
|
13
|
-
* value or duplicate keys.
|
|
13
|
+
* value or duplicate keys.
|
|
14
|
+
*
|
|
15
|
+
* Note: an empty value (`--smart-account ""`) IS captured as an empty
|
|
16
|
+
* string so the caller can distinguish "flag omitted" from "flag passed
|
|
17
|
+
* empty" - a foot-gun: silently dropping empty values caused callers to
|
|
18
|
+
* believe the interpreter adapter was engaged when it was not. The next
|
|
19
|
+
* token is treated as a value iff it is present and does not start with
|
|
20
|
+
* `--`; tokens starting with `--` are never consumed as values. */
|
|
14
21
|
export declare function parsePairs(argv: ReadonlyArray<string>): Record<string, string>;
|
|
15
22
|
/** Read a JSON file, parse it, and return the value. Throws with a CLI-
|
|
16
23
|
* friendly error if the file is missing or malformed. */
|
package/dist/src/output.js
CHANGED
|
@@ -29,7 +29,14 @@ export function parseFlags(argv) {
|
|
|
29
29
|
}
|
|
30
30
|
/** Resolve `--value <v>` style pairs after the subcommand name. Returns
|
|
31
31
|
* an object keyed by the option name (without `--`). Throws on missing
|
|
32
|
-
* value or duplicate keys.
|
|
32
|
+
* value or duplicate keys.
|
|
33
|
+
*
|
|
34
|
+
* Note: an empty value (`--smart-account ""`) IS captured as an empty
|
|
35
|
+
* string so the caller can distinguish "flag omitted" from "flag passed
|
|
36
|
+
* empty" - a foot-gun: silently dropping empty values caused callers to
|
|
37
|
+
* believe the interpreter adapter was engaged when it was not. The next
|
|
38
|
+
* token is treated as a value iff it is present and does not start with
|
|
39
|
+
* `--`; tokens starting with `--` are never consumed as values. */
|
|
33
40
|
export function parsePairs(argv) {
|
|
34
41
|
const out = {};
|
|
35
42
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -41,11 +48,23 @@ export function parsePairs(argv) {
|
|
|
41
48
|
if (key && val !== undefined)
|
|
42
49
|
out[key] = val;
|
|
43
50
|
}
|
|
44
|
-
else if (a?.startsWith('--')
|
|
51
|
+
else if (a?.startsWith('--')) {
|
|
45
52
|
const key = a.slice(2);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
i
|
|
53
|
+
if (!key)
|
|
54
|
+
continue;
|
|
55
|
+
const next = argv[i + 1];
|
|
56
|
+
// Only consume the next token if it is present AND does not look like
|
|
57
|
+
// another flag. Empty strings DO count as values so callers can
|
|
58
|
+
// distinguish "omitted" from "passed empty".
|
|
59
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
60
|
+
out[key] = next;
|
|
61
|
+
i++;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
// Standalone flag (no value) - record as empty string so `!== undefined`
|
|
65
|
+
// checks upstream can detect presence.
|
|
66
|
+
out[key] = '';
|
|
67
|
+
}
|
|
49
68
|
}
|
|
50
69
|
}
|
|
51
70
|
return out;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// apps/policy-builder-cli/src/commands/record.ts
|
|
3
|
+
//
|
|
4
|
+
// `policy-builder record` subcommand. Thin wrapper around the core
|
|
5
|
+
// `recordTransaction` - no business logic, just argv -> RecordInput + the
|
|
6
|
+
// canonical CLI envelope.
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.runRecordCommand = runRecordCommand;
|
|
9
|
+
const run_1 = require("@crediolabs/policy-synth/run");
|
|
10
|
+
const output_ts_1 = require("../output.js");
|
|
11
|
+
async function runRecordCommand(argv, flags) {
|
|
12
|
+
const pairs = (0, output_ts_1.parsePairs)(argv);
|
|
13
|
+
const network = pairs.network;
|
|
14
|
+
if (!network) {
|
|
15
|
+
throw new output_ts_1.CliError({
|
|
16
|
+
code: 'CLI_MISSING_ARG',
|
|
17
|
+
message: 'record: --network <mainnet|testnet> is required',
|
|
18
|
+
severity: 'error',
|
|
19
|
+
retryable: false,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
if (!pairs.hash && !pairs.xdr) {
|
|
23
|
+
throw new output_ts_1.CliError({
|
|
24
|
+
code: 'CLI_MISSING_ARG',
|
|
25
|
+
message: 'record: exactly one of --hash <tx> or --xdr <b64> is required',
|
|
26
|
+
severity: 'error',
|
|
27
|
+
retryable: false,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
if (pairs.hash && pairs.xdr) {
|
|
31
|
+
throw new output_ts_1.CliError({
|
|
32
|
+
code: 'CLI_MISSING_ARG',
|
|
33
|
+
message: 'record: provide exactly one of --hash or --xdr, not both',
|
|
34
|
+
severity: 'error',
|
|
35
|
+
retryable: false,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
const args = { network };
|
|
39
|
+
if (pairs.hash)
|
|
40
|
+
args.hash = pairs.hash;
|
|
41
|
+
if (pairs.xdr)
|
|
42
|
+
args.xdr = pairs.xdr;
|
|
43
|
+
const res = await (0, run_1.runRecordTransaction)(args);
|
|
44
|
+
return (0, output_ts_1.formatToolResponse)(res, flags, 'record');
|
|
45
|
+
}
|