@crediolabs/policy-synth 0.1.17 → 0.2.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 +44 -246
- package/dist/adapters/interpreter/adapter.d.ts +6 -7
- package/dist/adapters/interpreter/adapter.js +28 -48
- package/dist/adapters/oz/adapter.js +12 -13
- package/dist/codegen/compile-gate.js +5 -0
- package/dist/codegen/template.js +17 -0
- package/dist/install/authority-overlap.d.ts +134 -0
- package/dist/install/authority-overlap.js +0 -0
- package/dist/install/build-add-context-rule.d.ts +8 -0
- package/dist/install/build-add-context-rule.js +17 -46
- package/dist/install/build-install-policy.d.ts +12 -5
- package/dist/install/build-install-policy.js +32 -23
- package/dist/install/build-merge-policy.d.ts +70 -0
- package/dist/install/build-merge-policy.js +130 -0
- package/dist/install/get-interpreter-info.js +2 -2
- package/dist/install/index.d.ts +3 -1
- package/dist/install/index.js +8 -1
- package/dist/install/oz-auth.js +5 -3
- package/dist/install/plan-merge-policy.d.ts +49 -0
- package/dist/install/plan-merge-policy.js +86 -0
- package/dist/install/read-account-rules.d.ts +100 -0
- package/dist/install/read-account-rules.js +283 -0
- package/dist/predicate/decode.js +1 -1
- package/dist/predicate/encode.js +171 -10
- package/dist/record/decode.js +5 -6
- package/dist/registry/protocols.d.ts +1 -1
- package/dist/registry/protocols.js +1 -1
- package/dist/review-card/builder.d.ts +6 -0
- package/dist/review-card/builder.js +9 -1
- package/dist/run/index.d.ts +104 -18
- package/dist/run/index.js +359 -75
- package/dist/run/schemas.d.ts +479 -18
- package/dist/run/schemas.js +132 -24
- package/dist/synth/compose-from-recording.d.ts +10 -15
- package/dist/synth/compose-from-recording.js +79 -124
- package/dist/synth/deny-cases.d.ts +5 -0
- package/dist/synth/deny-cases.js +52 -13
- package/dist/synth/evaluate.js +69 -119
- package/dist/synth/harness.d.ts +13 -1
- package/dist/synth/harness.js +22 -1
- package/dist/synth/index.d.ts +1 -1
- package/dist/synth/synthesize-from-recording.d.ts +31 -21
- package/dist/synth/synthesize-from-recording.js +129 -155
- package/dist-cjs/adapters/interpreter/adapter.d.ts +6 -7
- package/dist-cjs/adapters/interpreter/adapter.js +28 -48
- package/dist-cjs/adapters/oz/adapter.js +12 -13
- package/dist-cjs/codegen/compile-gate.js +5 -0
- package/dist-cjs/codegen/template.js +17 -0
- package/dist-cjs/install/authority-overlap.d.ts +134 -0
- package/dist-cjs/install/authority-overlap.js +0 -0
- package/dist-cjs/install/build-add-context-rule.d.ts +8 -0
- package/dist-cjs/install/build-add-context-rule.js +17 -45
- package/dist-cjs/install/build-install-policy.d.ts +12 -5
- package/dist-cjs/install/build-install-policy.js +32 -23
- package/dist-cjs/install/build-merge-policy.d.ts +70 -0
- package/dist-cjs/install/build-merge-policy.js +134 -0
- package/dist-cjs/install/get-interpreter-info.js +2 -2
- package/dist-cjs/install/index.d.ts +3 -1
- package/dist-cjs/install/index.js +24 -3
- package/dist-cjs/install/oz-auth.js +5 -3
- package/dist-cjs/install/plan-merge-policy.d.ts +49 -0
- package/dist-cjs/install/plan-merge-policy.js +90 -0
- package/dist-cjs/install/read-account-rules.d.ts +100 -0
- package/dist-cjs/install/read-account-rules.js +296 -0
- package/dist-cjs/predicate/decode.js +1 -1
- package/dist-cjs/predicate/encode.js +171 -10
- package/dist-cjs/record/decode.js +5 -6
- package/dist-cjs/registry/protocols.d.ts +1 -1
- package/dist-cjs/registry/protocols.js +1 -1
- package/dist-cjs/review-card/builder.d.ts +6 -0
- package/dist-cjs/review-card/builder.js +9 -1
- package/dist-cjs/run/index.d.ts +104 -18
- package/dist-cjs/run/index.js +360 -74
- package/dist-cjs/run/schemas.d.ts +479 -18
- package/dist-cjs/run/schemas.js +133 -25
- package/dist-cjs/synth/compose-from-recording.d.ts +10 -15
- package/dist-cjs/synth/compose-from-recording.js +79 -124
- package/dist-cjs/synth/deny-cases.d.ts +5 -0
- package/dist-cjs/synth/deny-cases.js +52 -13
- package/dist-cjs/synth/evaluate.js +69 -119
- package/dist-cjs/synth/harness.d.ts +13 -1
- package/dist-cjs/synth/harness.js +22 -1
- package/dist-cjs/synth/index.d.ts +1 -1
- package/dist-cjs/synth/synthesize-from-recording.d.ts +31 -21
- package/dist-cjs/synth/synthesize-from-recording.js +129 -153
- package/package.json +1 -1
- package/src/adapters/interpreter/adapter.ts +28 -48
- package/src/adapters/oz/adapter.ts +12 -13
- package/src/codegen/compile-gate.ts +5 -0
- package/src/codegen/template.ts +17 -0
- package/src/install/authority-overlap.ts +0 -0
- package/src/install/build-add-context-rule.ts +29 -56
- package/src/install/build-install-policy.ts +41 -23
- package/src/install/build-merge-policy.ts +219 -0
- package/src/install/get-interpreter-info.ts +2 -2
- package/src/install/index.ts +36 -2
- package/src/install/oz-auth.ts +5 -3
- package/src/install/plan-merge-policy.ts +133 -0
- package/src/install/read-account-rules.ts +376 -0
- package/src/predicate/decode.ts +1 -1
- package/src/predicate/encode.ts +176 -10
- package/src/record/decode.ts +5 -6
- package/src/registry/protocols.ts +1 -1
- package/src/review-card/builder.ts +17 -1
- package/src/run/index.ts +500 -102
- package/src/run/schemas.ts +145 -24
- package/src/synth/compose-from-recording.ts +87 -132
- package/src/synth/deny-cases.ts +59 -13
- package/src/synth/evaluate.ts +70 -118
- package/src/synth/harness.ts +29 -1
- package/src/synth/index.ts +1 -0
- package/src/synth/synthesize-from-recording.ts +209 -192
- package/src/contracts/policy-template/OZ_POLICY_TRAIT.md +0 -196
package/README.md
CHANGED
|
@@ -1,272 +1,70 @@
|
|
|
1
1
|
# @crediolabs/policy-synth
|
|
2
2
|
|
|
3
|
-
Off-chain
|
|
3
|
+
Off-chain synthesis core for the OctoGate policy layer on Stellar. It turns a
|
|
4
|
+
transaction you recorded, or a mandate you can state in one sentence, into the
|
|
5
|
+
minimal on-chain policy that permits exactly that flow, then verifies,
|
|
6
|
+
simulates, and packages it for installation on an
|
|
7
|
+
[OpenZeppelin Stellar smart account](https://docs.openzeppelin.com/stellar-contracts).
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
constraint shapes OZ built-ins cannot express (exact ordered swap paths, oracle
|
|
9
|
-
price bounds, per-method scoping, recipient allowlists), the **interpreter
|
|
10
|
-
adapter** is opted in to emit a parallel predicate-shaped `PolicyDocument` that
|
|
11
|
-
installs alongside the OZ primitives. The synthesis is self-verified end-to-end
|
|
12
|
-
via `simulatePolicy` / `verifyPolicy` and the runner of the deny-case harness
|
|
13
|
-
before any bytes are emitted.
|
|
14
|
-
|
|
15
|
-
The package is pure ESM, node-compatible, and has a single runtime dependency
|
|
16
|
-
(`@stellar/stellar-sdk`). MIT-licensed.
|
|
9
|
+
The on-chain half is the `policy-interpreter` Soroban contract, which lives in
|
|
10
|
+
the same repository:
|
|
11
|
+
[untangledfinance/octogate](https://github.com/untangledfinance/octogate).
|
|
17
12
|
|
|
18
13
|
## Install
|
|
19
14
|
|
|
20
15
|
```sh
|
|
21
16
|
npm install @crediolabs/policy-synth
|
|
17
|
+
# or
|
|
18
|
+
bun add @crediolabs/policy-synth
|
|
22
19
|
```
|
|
23
20
|
|
|
24
|
-
##
|
|
25
|
-
|
|
26
|
-
There are two front-ends, both co-equal:
|
|
27
|
-
|
|
28
|
-
- **Recording** — decode a real transaction, then infer the minimal policy.
|
|
29
|
-
- **Mandate** — a declarative spec that lowers deterministically (no inference).
|
|
30
|
-
|
|
31
|
-
```ts
|
|
32
|
-
import {
|
|
33
|
-
recordTransaction,
|
|
34
|
-
synthesizeFromRecording,
|
|
35
|
-
synthesizeFromMandate,
|
|
36
|
-
placeholderOzConfig,
|
|
37
|
-
type MandateSpec,
|
|
38
|
-
} from '@crediolabs/policy-synth'
|
|
39
|
-
|
|
40
|
-
const oz = placeholderOzConfig('mainnet')
|
|
41
|
-
|
|
42
|
-
// --- Recording front-end -------------------------------------------------
|
|
43
|
-
const recorded = await recordTransaction({ network: 'mainnet', hash: '<tx-hash>' })
|
|
44
|
-
if (!recorded.ok) throw new Error(recorded.error.message)
|
|
45
|
-
|
|
46
|
-
const inferred = synthesizeFromRecording(recorded.data, { network: 'mainnet' }, oz)
|
|
47
|
-
if (inferred.ok) console.log(inferred.data)
|
|
48
|
-
|
|
49
|
-
// --- Mandate front-end (deterministic) -----------------------------------
|
|
50
|
-
const spec: MandateSpec = {
|
|
51
|
-
chain: 'stellar',
|
|
52
|
-
contract: 'CTOKEN',
|
|
53
|
-
method: 'transfer',
|
|
54
|
-
spendingLimit: { token: 'CTOKEN', limit: '5000000', windowSeconds: 2592000 },
|
|
55
|
-
}
|
|
56
|
-
const deterministic = synthesizeFromMandate(spec, oz)
|
|
57
|
-
if (deterministic.ok) console.log(deterministic.data)
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
Every entry point returns a discriminated `ToolResponse<T>`:
|
|
61
|
-
|
|
62
|
-
```ts
|
|
63
|
-
type ToolResponse<T> = { ok: true; data: T } | { ok: false; error: ToolError }
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
`ToolError` carries a machine-readable `code`, a `message`, a `severity`, and a
|
|
67
|
-
`retryable` flag, so callers (and agents) can branch without parsing prose.
|
|
68
|
-
|
|
69
|
-
### Recording modes and the confidence gate
|
|
70
|
-
|
|
71
|
-
- **On-chain** (`hash` set): fetched via the injected RPC fetcher (default: the
|
|
72
|
-
public Soroban RPC for the requested `network`). Pass your own `fetcher` to use
|
|
73
|
-
a custom endpoint or to test offline.
|
|
74
|
-
- **Simulation / XDR** (`xdr` set): the envelope XDR is decoded directly.
|
|
21
|
+
## What it does
|
|
75
22
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
the default threshold; to accept a simulation-only recording, pass an explicit
|
|
79
|
-
`confidenceOverride` that clears the gate.
|
|
23
|
+
The `@crediolabs/policy-synth/run` entry point exposes the seven tool bodies
|
|
24
|
+
that also back the CLI and the MCP server:
|
|
80
25
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
the
|
|
90
|
-
predicate `PolicyDocument` and merges it with the OZ refs. The byte blob on
|
|
91
|
-
the wire is the canonical XDR the on-chain interpreter will consume.
|
|
92
|
-
|
|
93
|
-
The opt-in is purely additive — every `ToolResponse` shape is unchanged; the
|
|
94
|
-
two new fields are `policyDocuments` (the predicate-shaped interpreter doc)
|
|
95
|
-
and one `policyRef` of `kind: 'interpreter'`.
|
|
26
|
+
| Function | Purpose |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| `runRecordTransaction` | Decode a Soroban transaction (on-chain hash or base64 envelope XDR) into a `RecordedTransaction`. |
|
|
29
|
+
| `runSynthesizePolicy` | Synthesise a `ProposedPolicy` from a recording or a deterministic `MandateSpec`. |
|
|
30
|
+
| `runSimulatePolicy` | Replay a recording against a proposed predicate and run the deny-case battery. |
|
|
31
|
+
| `runVerifyPolicy` | Static minimality check: every conjunct must be load-bearing. |
|
|
32
|
+
| `runInstallPolicy` | Build the unsigned `add_context_rule` transaction XDR for the smart account. |
|
|
33
|
+
| `runRevokePolicy` | Build the unsigned `remove_context_rule` transaction XDR. |
|
|
34
|
+
| `runGetInterpreterInfo` | Report the pinned interpreter address, grammar version and wasm sha256, optionally checked live over RPC. |
|
|
96
35
|
|
|
97
36
|
```ts
|
|
98
|
-
import {
|
|
99
|
-
import {
|
|
100
|
-
synthesizeFromRecording,
|
|
101
|
-
placeholderOzConfig,
|
|
102
|
-
} from '@crediolabs/policy-synth'
|
|
103
|
-
|
|
104
|
-
const oz = placeholderOzConfig('mainnet')
|
|
105
|
-
const smartAccount = Address.contract(Buffer.alloc(32, 0xee)).toString()
|
|
106
|
-
|
|
107
|
-
const result = synthesizeFromRecording(
|
|
108
|
-
recordedTx,
|
|
109
|
-
{
|
|
110
|
-
network: 'mainnet',
|
|
111
|
-
userResponses: {
|
|
112
|
-
windowSeconds: 2592000, // 30 days
|
|
113
|
-
limitAmount: '1000000000', // supplied cap
|
|
114
|
-
validUntilLedger: 200000000, // future ledger
|
|
115
|
-
oraclePriceBound: [ // optional oracle bound
|
|
116
|
-
{ asset: 'CEURC', operator: 'lt', value: '1000000000' },
|
|
117
|
-
],
|
|
118
|
-
swapRecipientAllowlist: ['GOWNER'], // optional allowlist
|
|
119
|
-
},
|
|
120
|
-
interpreter: {
|
|
121
|
-
smartAccountAddress: smartAccount, // MUST be a C... contract address
|
|
122
|
-
installNonce: 1, // first install -> 1
|
|
123
|
-
// oracleParams: { maxStalenessSeconds: 60, maxDeviationBps: 100 }
|
|
124
|
-
// (tighten-only vs the wasm defaults; widening is rejected)
|
|
125
|
-
},
|
|
126
|
-
},
|
|
127
|
-
oz
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
if (result.ok) {
|
|
131
|
-
console.log(result.data.policyDocuments.length) // >= 1 when constraints are routable
|
|
132
|
-
const interpreterRef = result.data.policyRefs.find((r) => r.kind === 'interpreter')
|
|
133
|
-
console.log(interpreterRef?.predicateBlobBase64) // canonical XDR, base64
|
|
134
|
-
console.log(result.data.contextRule.validUntilLedger)
|
|
135
|
-
}
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
The interpreter compile path is **fail-closed**:
|
|
139
|
-
|
|
140
|
-
- `SCOPE_SELF_CALL` — the call's recipient equals the smart account.
|
|
141
|
-
- `ORACLE_LEAF_INVALID_POSITION` — an oracle leaf is wrongly nested.
|
|
142
|
-
- `ORACLE_PARAMS_OUT_OF_RANGE` — `oracleParams` widening vs the wasm defaults.
|
|
143
|
-
- `SYNTHESIS_ERROR` — the interpreter IR is not fully covered.
|
|
144
|
-
- `DENY_CASE_FAILURE` — the emitted predicate fails the deny-case battery;
|
|
145
|
-
`details.failures` lists the flipped dimension(s).
|
|
146
|
-
|
|
147
|
-
A recorded swap that compiles to a permissive policy under OZ alone therefore
|
|
148
|
-
stays permissive unless the interpreter opt-in is supplied AND the predicate
|
|
149
|
-
self-verifies end-to-end.
|
|
37
|
+
import { runRecordTransaction, runSynthesizePolicy } from '@crediolabs/policy-synth/run'
|
|
150
38
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
1. The adapter emits the candidate predicate.
|
|
156
|
-
2. The synth builds a permit `EvalContext` from the recorded transaction (the
|
|
157
|
-
only call the user actually performed).
|
|
158
|
-
3. The candidate is **minimised** — load-bearing-free top-level conjuncts are
|
|
159
|
-
dropped (`and` predicates only; other shapes are returned unchanged).
|
|
160
|
-
4. The minimised predicate is run through the deny-case battery — a structural
|
|
161
|
-
fingerprint across `contract`, `function`, `args`, `amount`, `window`,
|
|
162
|
-
`oracle`, `recipient`, `frequency`. Each case must deny.
|
|
163
|
-
5. The intended recorded call is evaluated against the predicate; it must
|
|
164
|
-
permit.
|
|
165
|
-
6. The (possibly minimised) predicate is re-encoded; the canonical bytes + the
|
|
166
|
-
SHA-256 hash are stamped back onto the `PolicyDocument` and the
|
|
167
|
-
`interpreter` `policyRef`.
|
|
168
|
-
|
|
169
|
-
A successful `ok: true` is the proof that the emitted document is minimal AND
|
|
170
|
-
self-verified. A failure surfaces the matching gate code (see above).
|
|
171
|
-
|
|
172
|
-
## Simulate and verify (the `verify/` surface)
|
|
173
|
-
|
|
174
|
-
The same self-verify pipeline is exposed as a public API for callers that want
|
|
175
|
-
to re-run a check on a proposed predicate without re-synthesising:
|
|
176
|
-
|
|
177
|
-
```ts
|
|
178
|
-
import { simulatePolicy, verifyPolicy } from '@crediolabs/policy-synth'
|
|
179
|
-
|
|
180
|
-
// Runtime check: re-evaluate the predicate against the recorded call.
|
|
181
|
-
const runtime = simulatePolicy(predicate, recordedTx, {
|
|
182
|
-
validUntilLedger: 200000000,
|
|
183
|
-
oraclePricesByAsset: { CEURC: { price: '999999999', timestampSeconds: now } },
|
|
39
|
+
const recorded = await runRecordTransaction({
|
|
40
|
+
network: 'testnet',
|
|
41
|
+
hash: '<transaction hash>',
|
|
184
42
|
})
|
|
185
|
-
|
|
186
|
-
// Static minimality check: prove no top-level conjunct is load-bearing-free.
|
|
187
|
-
const staticCheck = verifyPolicy(predicate, recordedTx)
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
The boundary is pinned:
|
|
191
|
-
|
|
192
|
-
- `SIMULATION_ERROR` — runtime evaluation failed (malformed fixture, missing
|
|
193
|
-
oracle price, uncontrolled throw). The policy may still be minimal.
|
|
194
|
-
- `VERIFICATION_FAILED` — the static minimality check failed. The policy is
|
|
195
|
-
structurally over-broad regardless of how any concrete call evaluates.
|
|
196
|
-
|
|
197
|
-
Both are deterministic: same `(predicate, recordedTx, opts)` → byte-identical
|
|
198
|
-
envelope.
|
|
199
|
-
|
|
200
|
-
## Review-card (the human-audit surface)
|
|
201
|
-
|
|
202
|
-
The package emits a deterministic review-card summary so a human auditor can
|
|
203
|
-
sanity-check the inferred policy without re-running the synthesis:
|
|
204
|
-
|
|
205
|
-
```ts
|
|
206
|
-
import {
|
|
207
|
-
buildReviewCardSummary,
|
|
208
|
-
classifyConflict,
|
|
209
|
-
summaryCrossCheck,
|
|
210
|
-
} from '@crediolabs/policy-synth'
|
|
211
|
-
|
|
212
|
-
const summary = buildReviewCardSummary(proposedPolicy, recordedTx)
|
|
213
|
-
const conflict = classifyConflict(proposedPolicy, recordedTx)
|
|
214
|
-
const crossCheck = summaryCrossCheck(proposedPolicy, recordedTx)
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
The summary is the canonical human-readable digest of the proposed policy;
|
|
218
|
-
`classifyConflict` flags refs that contradict the recording; `summaryCrossCheck`
|
|
219
|
-
re-derives the summary from the raw refs and the recording, and reports
|
|
220
|
-
discrepancies. All three are pure and deterministic.
|
|
221
|
-
|
|
222
|
-
## Codegen escape hatch (the Rust interpreter)
|
|
223
|
-
|
|
224
|
-
When the canonical verifier is unavailable, the recorder can emit a Rust
|
|
225
|
-
source file that performs the same predicate evaluation off-chain via a
|
|
226
|
-
`cargo` build. The escape hatch is OUT of the audited happy path: the
|
|
227
|
-
synthesiser never calls `generateRust` itself; the CLI subcommand is the only
|
|
228
|
-
entry point.
|
|
229
|
-
|
|
230
|
-
```ts
|
|
231
|
-
import { generateRust, compileCheck, hasRustToolchain } from '@crediolabs/policy-synth'
|
|
232
|
-
|
|
233
|
-
if (await hasRustToolchain()) {
|
|
234
|
-
const { source, path } = generateRust(predicate, { out: 'policy.rs' })
|
|
235
|
-
const gate = await compileCheck({ crateDir: '.', predicate })
|
|
236
|
-
if (!gate.ok) console.error('compile gate failed:', gate.error)
|
|
237
|
-
}
|
|
238
43
|
```
|
|
239
44
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
predicate for parity testing.
|
|
45
|
+
Every function takes untrusted input, validates it through Zod schemas, and
|
|
46
|
+
returns a machine-readable response envelope instead of throwing - the same
|
|
47
|
+
contract whether the caller is a human script, the CLI, or an agent on the
|
|
48
|
+
other side of MCP.
|
|
245
49
|
|
|
246
|
-
|
|
50
|
+
Nothing in this package holds key material. Install and revoke return
|
|
51
|
+
*unsigned* XDR; the wallet's signature is the confirmation step.
|
|
247
52
|
|
|
248
|
-
|
|
249
|
-
`[interpreterRef?, ...oz_builtinRefs]` and bounded by
|
|
250
|
-
`OZ_LIMITS.maxPoliciesPerRule` (5). The orchestrator refuses to install a
|
|
251
|
-
policy that exceeds this cap (`POLICY_CAP_EXCEEDED`). The OZ-side `uncovered`
|
|
252
|
-
warnings that the interpreter actually lowered (per-method scoping, recipient
|
|
253
|
-
allowlists, exact ordered sequences, oracle price bounds, invocation-count
|
|
254
|
-
windows, token-mismatch spending limits) are dropped from the user-facing
|
|
255
|
-
warnings when the interpreter succeeds — the warning list reflects what is
|
|
256
|
-
still UN-enforced, not what OZ alone could not do.
|
|
53
|
+
## Security model
|
|
257
54
|
|
|
258
|
-
|
|
55
|
+
The synthesiser is the convenience layer; enforcement lives on chain in the
|
|
56
|
+
policy interpreter, whose deployed addresses and wasm sha256 are pinned in
|
|
57
|
+
`src/run/schemas.ts` and checked against the live network on install. The
|
|
58
|
+
[architecture document](https://github.com/untangledfinance/octogate/blob/main/docs/architecture.md)
|
|
59
|
+
is specific about what is and is not enforced; the audit status of the
|
|
60
|
+
contracts is stated in the
|
|
61
|
+
[repository README](https://github.com/untangledfinance/octogate#readme).
|
|
259
62
|
|
|
260
|
-
|
|
261
|
-
front-ends, the OZ Accounts adapter, the interpreter adapter, the
|
|
262
|
-
predicate encoder, the evaluator, the deny-case battery, the minimiser, the
|
|
263
|
-
self-verify pipeline, the simulate / verify surface, the review-card
|
|
264
|
-
builder, the cross-check, and the Rust codegen escape hatch.
|
|
63
|
+
## Related packages
|
|
265
64
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
package).
|
|
65
|
+
- [`@crediolabs/policy-builder-cli`](https://www.npmjs.com/package/@crediolabs/policy-builder-cli) - command-line record + synthesize.
|
|
66
|
+
- [`@crediolabs/policy-builder-mcp`](https://www.npmjs.com/package/@crediolabs/policy-builder-mcp) - the same tools over MCP for agents.
|
|
269
67
|
|
|
270
68
|
## License
|
|
271
69
|
|
|
272
|
-
MIT
|
|
70
|
+
MIT
|
|
@@ -4,7 +4,7 @@ import type { Network, PredicateNode } from '../../types.ts';
|
|
|
4
4
|
/** [VERIFY] NOT a real deployed address. The interpreter is a per-network
|
|
5
5
|
* deploy artifact we do not have yet; install is a later phase. */
|
|
6
6
|
export declare const PLACEHOLDER_INTERPRETER_ADDRESS = "VERIFY-interpreter-address";
|
|
7
|
-
/** Wasm-level oracle defaults (mirrors
|
|
7
|
+
/** Wasm-level oracle defaults (mirrors the interpreter's `OracleParams`).
|
|
8
8
|
* Per-policy overrides may TIGHTEN only - they may never exceed these. */
|
|
9
9
|
export declare const ORACLE_DEFAULTS: {
|
|
10
10
|
readonly maxStalenessSeconds: 600;
|
|
@@ -29,10 +29,9 @@ export interface InterpreterAdapterConfig {
|
|
|
29
29
|
export declare function createInterpreterAdapter(config: InterpreterAdapterConfig): CustodyAdapter;
|
|
30
30
|
/** Lower a single IR rule to the canonical pre-encoding `PredicateNode`. The
|
|
31
31
|
* orchestrator uses this to wire the self-verify + minimise pipeline: after
|
|
32
|
-
* `compile(ir)` succeeds
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* `
|
|
37
|
-
* here; callers that need it should use the result of `compile(ir)`. */
|
|
32
|
+
* `compile(ir)` succeeds it re-derives the PredicateNode via this helper to
|
|
33
|
+
* drive `minimize` and `runHarness` on the SAME shape the encoder saw. Pure
|
|
34
|
+
* and deterministic: same `rule + config` -> byte-identical PredicateNode.
|
|
35
|
+
* The `uncovered` list is NOT re-derived - callers needing it must use
|
|
36
|
+
* `compile(ir)`. */
|
|
38
37
|
export declare function lowerRuleToPredicate(rule: IRPolicyRule, config: InterpreterAdapterConfig): PredicateNode;
|
|
@@ -1,26 +1,9 @@
|
|
|
1
1
|
// src/adapters/interpreter/adapter.ts - the interpreter-policy CustodyAdapter.
|
|
2
2
|
//
|
|
3
3
|
// Compiles a PolicyIR to a single interpreter `PolicyDocument` + `PolicyRef`
|
|
4
|
-
// carrying the canonical predicate encoding
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// cover the canonical language subset. The compose step (P3) routes IR
|
|
8
|
-
// constructs between them.
|
|
9
|
-
//
|
|
10
|
-
// What it lowers (every IR construct the predicate DSL expresses):
|
|
11
|
-
// - IRLogic and/or -> PredicateNode and/or
|
|
12
|
-
// - IRLogic not -> PredicateNode not
|
|
13
|
-
// - IRCompare -> selector(left) vs literal(right), op carried over
|
|
14
|
-
// - IR `in` -> needle=selector, haystack=literals (PURESET
|
|
15
|
-
// membership; the haystack is always sorted by the
|
|
16
|
-
// encoder)
|
|
17
|
-
// - IR `eq_seq` -> eq(selectorLeaf, literal_vec([...])) - exact ordered
|
|
18
|
-
// sequence equality; element order is preserved
|
|
19
|
-
// verbatim (the encoder does NOT sort the vec)
|
|
20
|
-
// - IRSelector -> matching PredicateLeaf (see lowerSelector)
|
|
21
|
-
// - scope.contract -> sibling `call_contract == <contract>` (always
|
|
22
|
-
// emitted when set)
|
|
23
|
-
// - scope.method -> sibling `call_fn == <method>`
|
|
4
|
+
// carrying the canonical predicate encoding from `predicate/encode.ts`. Second
|
|
5
|
+
// backend (the OZ built-in adapter is the first); the compose step (P3) routes
|
|
6
|
+
// IR constructs between them.
|
|
24
7
|
//
|
|
25
8
|
// Three fail-closed enforcement gates (per spec):
|
|
26
9
|
// - oracle_price leaves MUST sit directly under the top-level `and`; nesting
|
|
@@ -38,7 +21,7 @@ import { encodePredicate } from "../../predicate/encode.js";
|
|
|
38
21
|
/** [VERIFY] NOT a real deployed address. The interpreter is a per-network
|
|
39
22
|
* deploy artifact we do not have yet; install is a later phase. */
|
|
40
23
|
export const PLACEHOLDER_INTERPRETER_ADDRESS = 'VERIFY-interpreter-address';
|
|
41
|
-
/** Wasm-level oracle defaults (mirrors
|
|
24
|
+
/** Wasm-level oracle defaults (mirrors the interpreter's `OracleParams`).
|
|
42
25
|
* Per-policy overrides may TIGHTEN only - they may never exceed these. */
|
|
43
26
|
export const ORACLE_DEFAULTS = {
|
|
44
27
|
maxStalenessSeconds: 600,
|
|
@@ -47,17 +30,15 @@ export const ORACLE_DEFAULTS = {
|
|
|
47
30
|
const CAPABILITIES = {
|
|
48
31
|
supportsSpendWindow: true,
|
|
49
32
|
supportsThreshold: false, // thresholds are the OZ adapter's job
|
|
50
|
-
//
|
|
51
|
-
// leaf at install.
|
|
52
|
-
// validUntilLedger, which the smart account enforces - not as a predicate.
|
|
33
|
+
// Expiry is via the context rule's validUntilLedger, not a predicate - the
|
|
34
|
+
// interpreter refuses a `valid_until` leaf at install.
|
|
53
35
|
supportsTimeExpiry: false,
|
|
54
36
|
supportsOraclePrice: true,
|
|
55
37
|
supportsInvocationCount: true,
|
|
56
38
|
supportsGeneralPredicate: true,
|
|
57
39
|
};
|
|
58
|
-
/** Parse confidence for a deterministic (non-decoded) input: full
|
|
59
|
-
*
|
|
60
|
-
* not applicable and confidence is 1. */
|
|
40
|
+
/** Parse confidence for a deterministic (non-decoded) input: full (1.0). A
|
|
41
|
+
* mandate needs no decoding, so the gate is not applicable. */
|
|
61
42
|
const FULL_PARSE_CONFIDENCE = {
|
|
62
43
|
overall: 1,
|
|
63
44
|
knownContracts: [],
|
|
@@ -77,12 +58,11 @@ export function createInterpreterAdapter(config) {
|
|
|
77
58
|
}
|
|
78
59
|
/** Lower a single IR rule to the canonical pre-encoding `PredicateNode`. The
|
|
79
60
|
* orchestrator uses this to wire the self-verify + minimise pipeline: after
|
|
80
|
-
* `compile(ir)` succeeds
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* `
|
|
85
|
-
* here; callers that need it should use the result of `compile(ir)`. */
|
|
61
|
+
* `compile(ir)` succeeds it re-derives the PredicateNode via this helper to
|
|
62
|
+
* drive `minimize` and `runHarness` on the SAME shape the encoder saw. Pure
|
|
63
|
+
* and deterministic: same `rule + config` -> byte-identical PredicateNode.
|
|
64
|
+
* The `uncovered` list is NOT re-derived - callers needing it must use
|
|
65
|
+
* `compile(ir)`. */
|
|
86
66
|
export function lowerRuleToPredicate(rule, config) {
|
|
87
67
|
return lowerRule(rule, config).predicate;
|
|
88
68
|
}
|
|
@@ -132,9 +112,9 @@ function compile(ir, config) {
|
|
|
132
112
|
function lowerRule(rule, config) {
|
|
133
113
|
const uncovered = [];
|
|
134
114
|
// scope -> context rule + sibling predicates. contract/method each become
|
|
135
|
-
// their own `eq` leaf and are merged into the top-level and alongside the
|
|
136
|
-
// constraints.
|
|
137
|
-
// position rule
|
|
115
|
+
// their own `eq` leaf and are merged into the top-level `and` alongside the
|
|
116
|
+
// constraints. The top-level MUST be `and` so oracle leaves sit directly
|
|
117
|
+
// under it (mandatory per the oracle position rule + canonical hash stability).
|
|
138
118
|
const scopeContract = rule.scope.contract;
|
|
139
119
|
const scopeMethod = rule.scope.method;
|
|
140
120
|
if (rule.scope.chainId !== undefined) {
|
|
@@ -242,18 +222,18 @@ function unsupportedConstruct(cond) {
|
|
|
242
222
|
return 'EVM calldata comparison (predicate DSL)';
|
|
243
223
|
if (s.kind === 'value')
|
|
244
224
|
return 'tx.value comparison (predicate DSL)';
|
|
245
|
-
// The on-chain interpreter sees ONE authorized call -
|
|
246
|
-
// `Context.sub_invocations` in v1 - so it cannot observe
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
225
|
+
// The on-chain interpreter sees ONE authorized call - no
|
|
226
|
+
// `Context.sub_invocations` in v1 - so it cannot observe token
|
|
227
|
+
// movements. `amount` has no value to read, and `window_spent`
|
|
228
|
+
// accumulates BY that amount, so its counter would never move.
|
|
229
|
+
// Deriving either from the call payload would quietly swap "value
|
|
250
230
|
// actually moved" for "value the caller declared" - a weaker guarantee
|
|
251
231
|
// than the review card would be claiming.
|
|
252
232
|
//
|
|
253
|
-
// Rolling spend caps belong to the OZ `spending_limit` primitive
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
//
|
|
233
|
+
// Rolling spend caps belong to the OZ `spending_limit` primitive (already
|
|
234
|
+
// audited, emitted by the OZ adapter). A per-call cap is expressible here
|
|
235
|
+
// as `arg_field`; bounding it with `invocation_count` gives an enforceable
|
|
236
|
+
// ceiling per window.
|
|
257
237
|
return unsourceableSelector(s);
|
|
258
238
|
}
|
|
259
239
|
// Recurse: a nested `and`/`or`/`not` must not smuggle a selector past the
|
|
@@ -367,9 +347,9 @@ function lowerSelector(s) {
|
|
|
367
347
|
case 'arg_field':
|
|
368
348
|
return { kind: 'call_arg_field', index: s.argIndex, element: s.element, field: s.field };
|
|
369
349
|
// `amount` / `window_spent` are filtered out by `unsupportedConstruct`
|
|
370
|
-
// before lowering - the interpreter cannot source either on chain.
|
|
371
|
-
// here means the pre-scan was bypassed
|
|
372
|
-
// leaf the contract will refuse.
|
|
350
|
+
// before lowering - the interpreter cannot source either on chain.
|
|
351
|
+
// Reaching here means the pre-scan was bypassed; fail loudly rather than
|
|
352
|
+
// emit a leaf the contract will refuse.
|
|
373
353
|
case 'amount':
|
|
374
354
|
case 'window_spent':
|
|
375
355
|
throw new Error(`interpreter adapter cannot lower \`${s.kind}\`: it should have been reported as uncovered`);
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// Anything needing a capability this backend lacks (oracle price, invocation
|
|
10
10
|
// count, per-arg comparison/allowlist, guard, nested boolean predicate) is NOT
|
|
11
11
|
// emitted: it is named in `uncovered` and `covered` is set false. Nothing is
|
|
12
|
-
// silently dropped
|
|
12
|
+
// silently dropped.
|
|
13
13
|
//
|
|
14
14
|
// OZ built-in policy instance addresses are per-network deploy artifacts we do
|
|
15
15
|
// not have yet (install is a later phase). They are injected via config; week-1
|
|
@@ -27,9 +27,8 @@ export const PLACEHOLDER_OZ_INSTANCES = {
|
|
|
27
27
|
export function placeholderOzConfig(network) {
|
|
28
28
|
return { network, instances: PLACEHOLDER_OZ_INSTANCES };
|
|
29
29
|
}
|
|
30
|
-
/** Parse confidence for a deterministic (non-decoded) input: full
|
|
31
|
-
*
|
|
32
|
-
* not applicable and confidence is 1. */
|
|
30
|
+
/** Parse confidence for a deterministic (non-decoded) input: full (1.0). A
|
|
31
|
+
* mandate needs no decoding, so the gate is not applicable. */
|
|
33
32
|
const FULL_PARSE_CONFIDENCE = {
|
|
34
33
|
overall: 1,
|
|
35
34
|
knownContracts: [],
|
|
@@ -83,10 +82,10 @@ function compile(ir, config) {
|
|
|
83
82
|
function lowerRule(rule, config) {
|
|
84
83
|
const uncovered = [];
|
|
85
84
|
const policyRefs = [];
|
|
86
|
-
// scope -> context rule type. OZ scopes by contract (CallContract); a
|
|
87
|
-
// method-level restriction is
|
|
88
|
-
//
|
|
89
|
-
//
|
|
85
|
+
// scope -> context rule type. OZ scopes by contract (CallContract); a
|
|
86
|
+
// method-level restriction is flagged as not covered because CallContract
|
|
87
|
+
// alone permits other methods on the same contract (e.g. an unbounded approve
|
|
88
|
+
// alongside a capped transfer).
|
|
90
89
|
const contextRuleType = rule.scope.contract !== undefined
|
|
91
90
|
? { kind: 'call_contract', contract: rule.scope.contract }
|
|
92
91
|
: { kind: 'default' };
|
|
@@ -115,8 +114,8 @@ function lowerRule(rule, config) {
|
|
|
115
114
|
uncovered.push(`guard: ${describeCondition(rule.guard)}`);
|
|
116
115
|
}
|
|
117
116
|
// constraints -> spending_limit where they match; else not covered. The OZ
|
|
118
|
-
// spending_limit
|
|
119
|
-
//
|
|
117
|
+
// spending_limit takes `{ spending_limit: i128, period_ledgers: u32 }` and
|
|
118
|
+
// has NO token param: it only accepts a CallContract context rule
|
|
120
119
|
// (OnlyCallContractAllowed) and limits transfers of that context's contract,
|
|
121
120
|
// so the spent token must equal the scope contract, and the window is a
|
|
122
121
|
// ledger count (~5s/ledger), not seconds.
|
|
@@ -144,7 +143,7 @@ function lowerRule(rule, config) {
|
|
|
144
143
|
}
|
|
145
144
|
// approval.threshold -> simple/weighted threshold primitive. A threshold < 1
|
|
146
145
|
// is not a real M-of-N gate (0 approvals authorises everything), so refuse to
|
|
147
|
-
// emit a no-op primitive and flag it as not covered
|
|
146
|
+
// emit a no-op primitive and flag it as not covered.
|
|
148
147
|
if (rule.approval) {
|
|
149
148
|
if (!Number.isInteger(rule.approval.threshold) || rule.approval.threshold < 1) {
|
|
150
149
|
uncovered.push(`approval threshold ${rule.approval.threshold} is not a positive integer (a 0 or negative threshold is not an M-of-N gate)`);
|
|
@@ -208,8 +207,8 @@ function matchSpendingLimit(c) {
|
|
|
208
207
|
function describeCondition(cond) {
|
|
209
208
|
switch (cond.op) {
|
|
210
209
|
case 'slippage_floor':
|
|
211
|
-
//
|
|
212
|
-
//
|
|
210
|
+
// OZ primitives bound a value against a constant; this bounds one call
|
|
211
|
+
// argument against another, which none of them can express.
|
|
213
212
|
return `slippage floor on arg[${cond.outArgIndex}] (OZ built-ins cannot bound one argument against another)`;
|
|
214
213
|
case 'in':
|
|
215
214
|
return `value allowlist on ${describeSelector(cond.selector)} (arg allowlist)`;
|
|
@@ -75,6 +75,11 @@ publish = false
|
|
|
75
75
|
|
|
76
76
|
[lib]
|
|
77
77
|
crate-type = ["cdylib"]
|
|
78
|
+
# The gate writes the source beside the manifest rather than under src/, so
|
|
79
|
+
# the path is stated. Without it cargo looks for src/lib.rs and fails to
|
|
80
|
+
# parse the manifest before it ever compiles anything, which reads as a
|
|
81
|
+
# broken generator rather than a broken scaffold.
|
|
82
|
+
path = "lib.rs"
|
|
78
83
|
|
|
79
84
|
[dependencies]
|
|
80
85
|
soroban-sdk = "22"
|
package/dist/codegen/template.js
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
// src/codegen/template.ts - deterministic Rust source generator for the OZ
|
|
2
2
|
// `Policy` escape-hatch skeleton.
|
|
3
3
|
//
|
|
4
|
+
// SUPERSEDED. The architecture this belongs to no longer exists. A deployed
|
|
5
|
+
// policy involves exactly two contracts, the OZ smart account and one
|
|
6
|
+
// immutable interpreter, and the synthesiser emits policy DATA rather than
|
|
7
|
+
// code. Nothing on the synthesis path reaches this module, and no install can
|
|
8
|
+
// produce a contract from it. When a constraint falls outside the grammar the
|
|
9
|
+
// answer is to version the synthesiser and the interpreter together, not to
|
|
10
|
+
// put unaudited Rust on chain per policy.
|
|
11
|
+
//
|
|
12
|
+
// Deleting it is a decision nobody has taken yet, not a dependency question.
|
|
13
|
+
// `compile-gate.ts` takes a Rust string and knows nothing about this module,
|
|
14
|
+
// and its tests compile a hardcoded fixture, so the gate keeps working either
|
|
15
|
+
// way. What deleting this would remove is the only in-repo producer of policy
|
|
16
|
+
// Rust for the gate to check. Do not read its presence as the product
|
|
17
|
+
// generating contracts, and do not wire it into the synthesis path.
|
|
18
|
+
//
|
|
19
|
+
// What follows describes the abandoned design.
|
|
20
|
+
//
|
|
4
21
|
// This is the LAST-RESORT tool for constraints the v1 DSL cannot express. It
|
|
5
22
|
// lives OUT of the audited happy-path surface; the synthesiser never emits it
|
|
6
23
|
// automatically. It mirrors Zodiac Roles' `Custom` operator: a hand-written
|