@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.
Files changed (113) hide show
  1. package/README.md +44 -246
  2. package/dist/adapters/interpreter/adapter.d.ts +6 -7
  3. package/dist/adapters/interpreter/adapter.js +28 -48
  4. package/dist/adapters/oz/adapter.js +12 -13
  5. package/dist/codegen/compile-gate.js +5 -0
  6. package/dist/codegen/template.js +17 -0
  7. package/dist/install/authority-overlap.d.ts +134 -0
  8. package/dist/install/authority-overlap.js +0 -0
  9. package/dist/install/build-add-context-rule.d.ts +8 -0
  10. package/dist/install/build-add-context-rule.js +17 -46
  11. package/dist/install/build-install-policy.d.ts +12 -5
  12. package/dist/install/build-install-policy.js +32 -23
  13. package/dist/install/build-merge-policy.d.ts +70 -0
  14. package/dist/install/build-merge-policy.js +130 -0
  15. package/dist/install/get-interpreter-info.js +2 -2
  16. package/dist/install/index.d.ts +3 -1
  17. package/dist/install/index.js +8 -1
  18. package/dist/install/oz-auth.js +5 -3
  19. package/dist/install/plan-merge-policy.d.ts +49 -0
  20. package/dist/install/plan-merge-policy.js +86 -0
  21. package/dist/install/read-account-rules.d.ts +100 -0
  22. package/dist/install/read-account-rules.js +283 -0
  23. package/dist/predicate/decode.js +1 -1
  24. package/dist/predicate/encode.js +171 -10
  25. package/dist/record/decode.js +5 -6
  26. package/dist/registry/protocols.d.ts +1 -1
  27. package/dist/registry/protocols.js +1 -1
  28. package/dist/review-card/builder.d.ts +6 -0
  29. package/dist/review-card/builder.js +9 -1
  30. package/dist/run/index.d.ts +104 -18
  31. package/dist/run/index.js +359 -75
  32. package/dist/run/schemas.d.ts +479 -18
  33. package/dist/run/schemas.js +132 -24
  34. package/dist/synth/compose-from-recording.d.ts +10 -15
  35. package/dist/synth/compose-from-recording.js +79 -124
  36. package/dist/synth/deny-cases.d.ts +5 -0
  37. package/dist/synth/deny-cases.js +52 -13
  38. package/dist/synth/evaluate.js +69 -119
  39. package/dist/synth/harness.d.ts +13 -1
  40. package/dist/synth/harness.js +22 -1
  41. package/dist/synth/index.d.ts +1 -1
  42. package/dist/synth/synthesize-from-recording.d.ts +31 -21
  43. package/dist/synth/synthesize-from-recording.js +129 -155
  44. package/dist-cjs/adapters/interpreter/adapter.d.ts +6 -7
  45. package/dist-cjs/adapters/interpreter/adapter.js +28 -48
  46. package/dist-cjs/adapters/oz/adapter.js +12 -13
  47. package/dist-cjs/codegen/compile-gate.js +5 -0
  48. package/dist-cjs/codegen/template.js +17 -0
  49. package/dist-cjs/install/authority-overlap.d.ts +134 -0
  50. package/dist-cjs/install/authority-overlap.js +0 -0
  51. package/dist-cjs/install/build-add-context-rule.d.ts +8 -0
  52. package/dist-cjs/install/build-add-context-rule.js +17 -45
  53. package/dist-cjs/install/build-install-policy.d.ts +12 -5
  54. package/dist-cjs/install/build-install-policy.js +32 -23
  55. package/dist-cjs/install/build-merge-policy.d.ts +70 -0
  56. package/dist-cjs/install/build-merge-policy.js +134 -0
  57. package/dist-cjs/install/get-interpreter-info.js +2 -2
  58. package/dist-cjs/install/index.d.ts +3 -1
  59. package/dist-cjs/install/index.js +24 -3
  60. package/dist-cjs/install/oz-auth.js +5 -3
  61. package/dist-cjs/install/plan-merge-policy.d.ts +49 -0
  62. package/dist-cjs/install/plan-merge-policy.js +90 -0
  63. package/dist-cjs/install/read-account-rules.d.ts +100 -0
  64. package/dist-cjs/install/read-account-rules.js +296 -0
  65. package/dist-cjs/predicate/decode.js +1 -1
  66. package/dist-cjs/predicate/encode.js +171 -10
  67. package/dist-cjs/record/decode.js +5 -6
  68. package/dist-cjs/registry/protocols.d.ts +1 -1
  69. package/dist-cjs/registry/protocols.js +1 -1
  70. package/dist-cjs/review-card/builder.d.ts +6 -0
  71. package/dist-cjs/review-card/builder.js +9 -1
  72. package/dist-cjs/run/index.d.ts +104 -18
  73. package/dist-cjs/run/index.js +360 -74
  74. package/dist-cjs/run/schemas.d.ts +479 -18
  75. package/dist-cjs/run/schemas.js +133 -25
  76. package/dist-cjs/synth/compose-from-recording.d.ts +10 -15
  77. package/dist-cjs/synth/compose-from-recording.js +79 -124
  78. package/dist-cjs/synth/deny-cases.d.ts +5 -0
  79. package/dist-cjs/synth/deny-cases.js +52 -13
  80. package/dist-cjs/synth/evaluate.js +69 -119
  81. package/dist-cjs/synth/harness.d.ts +13 -1
  82. package/dist-cjs/synth/harness.js +22 -1
  83. package/dist-cjs/synth/index.d.ts +1 -1
  84. package/dist-cjs/synth/synthesize-from-recording.d.ts +31 -21
  85. package/dist-cjs/synth/synthesize-from-recording.js +129 -153
  86. package/package.json +1 -1
  87. package/src/adapters/interpreter/adapter.ts +28 -48
  88. package/src/adapters/oz/adapter.ts +12 -13
  89. package/src/codegen/compile-gate.ts +5 -0
  90. package/src/codegen/template.ts +17 -0
  91. package/src/install/authority-overlap.ts +0 -0
  92. package/src/install/build-add-context-rule.ts +29 -56
  93. package/src/install/build-install-policy.ts +41 -23
  94. package/src/install/build-merge-policy.ts +219 -0
  95. package/src/install/get-interpreter-info.ts +2 -2
  96. package/src/install/index.ts +36 -2
  97. package/src/install/oz-auth.ts +5 -3
  98. package/src/install/plan-merge-policy.ts +133 -0
  99. package/src/install/read-account-rules.ts +376 -0
  100. package/src/predicate/decode.ts +1 -1
  101. package/src/predicate/encode.ts +176 -10
  102. package/src/record/decode.ts +5 -6
  103. package/src/registry/protocols.ts +1 -1
  104. package/src/review-card/builder.ts +17 -1
  105. package/src/run/index.ts +500 -102
  106. package/src/run/schemas.ts +145 -24
  107. package/src/synth/compose-from-recording.ts +87 -132
  108. package/src/synth/deny-cases.ts +59 -13
  109. package/src/synth/evaluate.ts +70 -118
  110. package/src/synth/harness.ts +29 -1
  111. package/src/synth/index.ts +1 -0
  112. package/src/synth/synthesize-from-recording.ts +209 -192
  113. 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 TypeScript synthesis core for the OpenZeppelin Accounts Policy Builder.
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
- It records a Soroban transaction (from an on-chain hash or a raw envelope XDR),
6
- synthesises the **minimal** policy that permits exactly that flow, and compiles it
7
- through the OZ Accounts adapter into a proposed policy. When the policy needs
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
- ## Usage
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
- XDR/simulation mode has no raw on-chain events, so the recorder cannot run its
77
- events cross-check and lowers `parseConfidence`. It therefore **fails closed** at
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
- ## Interpreter predicate emission (the `interpreter` opt-in)
82
-
83
- OZ built-ins express spending limits, simple thresholds, and weighted
84
- thresholds. They **cannot** express an exact ordered swap path, an oracle
85
- price bound, per-method scoping, a recipient allowlist, or an
86
- invocation-count window. The recording path surfaces those gaps as warnings
87
- by default ("Not covered by OZ built-in primitives: ..."). When the caller
88
- opts in to the **interpreter adapter**, the same constraint set is routed to
89
- the parallel interpreter IR; the adapter compiles it into a canonical
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 { Address } from '@stellar/stellar-sdk'
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
- ### Self-verify + minimise (always-on with the opt-in)
152
-
153
- Opting in to the interpreter also turns on the **self-verify pipeline**:
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
- The escape hatch is toolchain-gated: `hasRustToolchain()` returns `false`
241
- when `cargo` is not installed, and `compileCheck()` refuses to run on a
242
- machine without one. The generated source is **not** the on-chain interpreter
243
- it is a deterministic off-chain reference that re-evaluates the same
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
- ## Composition rules
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
- The merged `policyRefs` on a `ProposedPolicy` are ordered
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
- ## Status
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
- Implemented and unit-test covered: the recorder, both synthesizer
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
- The on-chain Rust interpreter, install-transaction assembly, and live RPC
267
- integration are wired in the @crediolabs/policy-builder-cli layer (separate
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 INTERPRETER_INSTALL_PARAMS.md OracleParams).
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 (`covered === true`, `proposed` set), the
33
- * orchestrator re-derives the PredicateNode via this helper to drive
34
- * `minimize` and `runHarness` on the SAME shape the encoder saw. Pure and
35
- * deterministic: same `rule + config` -> byte-identical PredicateNode. The
36
- * `uncovered` list the adapter surfaces during compile is NOT re-derived
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 defined in
5
- // `packages/policy-interpreter/INTERPRETER_INSTALL_PARAMS.md`. The adapter is
6
- // the second backend (the OZ built-in adapter is the first); together they
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 INTERPRETER_INSTALL_PARAMS.md OracleParams).
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
- // False for the predicate-leaf path: the interpreter refuses a `valid_until`
51
- // leaf at install. Expiry is still available, but as the context rule's
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, with an
59
- * empty unknown/opaque breakdown. A mandate needs no decoding, so the gate is
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 (`covered === true`, `proposed` set), the
81
- * orchestrator re-derives the PredicateNode via this helper to drive
82
- * `minimize` and `runHarness` on the SAME shape the encoder saw. Pure and
83
- * deterministic: same `rule + config` -> byte-identical PredicateNode. The
84
- * `uncovered` list the adapter surfaces during compile is NOT re-derived
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. This keeps the top-level shape `and` (mandatory per the oracle
137
- // position rule and the canonical hash stability for `and`).
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 - there is no
246
- // `Context.sub_invocations` in v1 - so it cannot observe the
247
- // transaction's token movements. `amount` has no value to read, and
248
- // `window_spent` accumulates BY that amount, so its counter would never
249
- // move. Deriving either from the call payload would quietly swap "value
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, which
254
- // is already audited and which the OZ adapter emits. A per-call cap is
255
- // expressible here as `arg_field`; bounding it with `invocation_count`
256
- // gives an enforceable ceiling per window.
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. Reaching
371
- // here means the pre-scan was bypassed, so fail loudly rather than emit a
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 - the uncovered constructs are named instead.
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, with an
31
- * empty unknown/opaque breakdown. A mandate needs no decoding, so the gate is
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 finer
87
- // method-level restriction is a predicate concern and must be flagged as not
88
- // covered because CallContract alone permits other methods on the same contract
89
- // (e.g. an unbounded approve alongside a capped transfer).
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 policy takes `{ spending_limit: i128, period_ledgers: u32 }`
119
- // and has NO token param: it only accepts a CallContract context rule
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 instead.
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
- // The OZ primitives bound a value against a constant; this bounds one
212
- // call argument against another, which none of them can express.
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"
@@ -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