@crediolabs/policy-synth 0.1.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 (85) hide show
  1. package/dist/adapters/oz/adapter.d.ts +20 -0
  2. package/dist/adapters/oz/adapter.js +282 -0
  3. package/dist/adapters/oz/index.d.ts +1 -0
  4. package/dist/adapters/oz/index.js +2 -0
  5. package/dist/errors.d.ts +37 -0
  6. package/dist/errors.js +2 -0
  7. package/dist/index.d.ts +9 -0
  8. package/dist/index.js +9 -0
  9. package/dist/ir/index.d.ts +1 -0
  10. package/dist/ir/index.js +2 -0
  11. package/dist/ir/types.d.ts +97 -0
  12. package/dist/ir/types.js +11 -0
  13. package/dist/mandate/index.d.ts +2 -0
  14. package/dist/mandate/index.js +2 -0
  15. package/dist/mandate/to-ir.d.ts +3 -0
  16. package/dist/mandate/to-ir.js +60 -0
  17. package/dist/mandate/types.d.ts +20 -0
  18. package/dist/mandate/types.js +8 -0
  19. package/dist/record/decode.d.ts +76 -0
  20. package/dist/record/decode.js +372 -0
  21. package/dist/record/freshness.d.ts +17 -0
  22. package/dist/record/freshness.js +50 -0
  23. package/dist/record/index.d.ts +21 -0
  24. package/dist/record/index.js +163 -0
  25. package/dist/record/movements.d.ts +20 -0
  26. package/dist/record/movements.js +187 -0
  27. package/dist/record/rpc.d.ts +22 -0
  28. package/dist/record/rpc.js +70 -0
  29. package/dist/record/validate.d.ts +22 -0
  30. package/dist/record/validate.js +60 -0
  31. package/dist/registry/identify.d.ts +11 -0
  32. package/dist/registry/identify.js +84 -0
  33. package/dist/registry/index.d.ts +3 -0
  34. package/dist/registry/index.js +4 -0
  35. package/dist/registry/known-addresses.d.ts +16 -0
  36. package/dist/registry/known-addresses.js +49 -0
  37. package/dist/registry/protocols.d.ts +38 -0
  38. package/dist/registry/protocols.js +149 -0
  39. package/dist/seams/index.d.ts +1 -0
  40. package/dist/seams/index.js +2 -0
  41. package/dist/seams/types.d.ts +66 -0
  42. package/dist/seams/types.js +11 -0
  43. package/dist/synth/compose-from-recording.d.ts +36 -0
  44. package/dist/synth/compose-from-recording.js +162 -0
  45. package/dist/synth/index.d.ts +5 -0
  46. package/dist/synth/index.js +6 -0
  47. package/dist/synth/lower.d.ts +23 -0
  48. package/dist/synth/lower.js +116 -0
  49. package/dist/synth/scope.d.ts +26 -0
  50. package/dist/synth/scope.js +77 -0
  51. package/dist/synth/synthesize-from-mandate.d.ts +5 -0
  52. package/dist/synth/synthesize-from-mandate.js +34 -0
  53. package/dist/synth/synthesize-from-recording.d.ts +17 -0
  54. package/dist/synth/synthesize-from-recording.js +178 -0
  55. package/dist/types.d.ts +249 -0
  56. package/dist/types.js +35 -0
  57. package/package.json +37 -0
  58. package/src/adapters/oz/adapter.ts +363 -0
  59. package/src/adapters/oz/index.ts +9 -0
  60. package/src/errors.ts +79 -0
  61. package/src/index.ts +9 -0
  62. package/src/ir/index.ts +13 -0
  63. package/src/ir/types.ts +94 -0
  64. package/src/mandate/index.ts +4 -0
  65. package/src/mandate/to-ir.ts +71 -0
  66. package/src/mandate/types.ts +21 -0
  67. package/src/record/decode.ts +500 -0
  68. package/src/record/freshness.ts +63 -0
  69. package/src/record/index.ts +224 -0
  70. package/src/record/movements.ts +188 -0
  71. package/src/record/rpc.ts +88 -0
  72. package/src/record/validate.ts +75 -0
  73. package/src/registry/identify.ts +99 -0
  74. package/src/registry/index.ts +24 -0
  75. package/src/registry/known-addresses.ts +71 -0
  76. package/src/registry/protocols.ts +176 -0
  77. package/src/seams/index.ts +11 -0
  78. package/src/seams/types.ts +81 -0
  79. package/src/synth/compose-from-recording.ts +226 -0
  80. package/src/synth/index.ts +19 -0
  81. package/src/synth/lower.ts +144 -0
  82. package/src/synth/scope.ts +108 -0
  83. package/src/synth/synthesize-from-mandate.ts +46 -0
  84. package/src/synth/synthesize-from-recording.ts +226 -0
  85. package/src/types.ts +218 -0
package/src/errors.ts ADDED
@@ -0,0 +1,79 @@
1
+ // src/errors.ts
2
+
3
+ export type ErrorCode =
4
+ // --- recorder ---
5
+ | 'RECORDING_FAILED'
6
+ | 'RECORDING_VALIDATION_FAILED'
7
+ // --- synthesis ---
8
+ | 'SCOPE_UNRESOLVED'
9
+ | 'SYNTHESIS_ERROR'
10
+ | 'MALFORMED_PREDICATE'
11
+ // --- simulate / verify ---
12
+ | 'SIMULATION_ERROR'
13
+ | 'VERIFICATION_FAILED'
14
+ | 'DENY_CASE_FAILURE'
15
+ | 'PERMIT_CASE_FAILED'
16
+ | 'SUMMARY_DRIFT'
17
+ // --- install / revoke ---
18
+ | 'INSTALL_BUILD_FAILED'
19
+ | 'INSTALL_CONFIRM_MISSING'
20
+ | 'INSTALL_CONFIRM_EXPIRED'
21
+ | 'REVOKE_BUILD_FAILED'
22
+ | 'REVOKE_CONFIRM_MISSING'
23
+ | 'USER_REJECTED_SIGN'
24
+ | 'WALLET_TIMEOUT'
25
+ | 'WALLET_UNAVAILABLE'
26
+ // --- predicate caps (fail-closed at install) ---
27
+ | 'PREDICATE_TOO_LARGE'
28
+ | 'PREDICATE_TOO_DEEP'
29
+ | 'PREDICATE_ORACLE_OVER_LIMIT'
30
+ | 'POLICY_CAP_EXCEEDED'
31
+ | 'WASM_TOO_LARGE'
32
+ // --- interpreter-side denies (canonical names; matches Rust DenyCode) ---
33
+ | 'MASTER_AUTH_REQUIRED'
34
+ | 'NONCE_REPLAY'
35
+ | 'VERSION_MISMATCH'
36
+ | 'ARITHMETIC_OVERFLOW'
37
+ | 'AMOUNT_OVERFLOW'
38
+ | 'RULE_SIGNERS_CHANGED'
39
+ | 'SCOPE_SELF_CALL'
40
+ // --- oracle denies (fatal evaluator errors) ---
41
+ | 'ORACLE_STALE'
42
+ | 'ORACLE_MISSING'
43
+ | 'ORACLE_DEVIATION_EXCEEDED'
44
+ | 'ORACLE_MALFORMED_HISTORY'
45
+ | 'ORACLE_FINGERPRINT_DRIFT'
46
+ | 'ORACLE_PAUSED'
47
+ | 'ORACLE_LEAF_INVALID_POSITION'
48
+ | 'ORACLE_PARAMS_OUT_OF_RANGE'
49
+ | 'ORACLE_DECIMALS_MISMATCH'
50
+ // --- escape-hatch compile gate ---
51
+ | 'COMPILE_OK'
52
+ | 'COMPILE_GATE_FAILED'
53
+
54
+ export interface ToolError {
55
+ code: ErrorCode
56
+ message: string
57
+ /** LLM-actionable hint for the agent skill. Per-code severity is pinned
58
+ * in CODE_SEVERITY below; this field is the resolved value at runtime. */
59
+ severity: 'info' | 'warning' | 'error' | 'fatal'
60
+ retryable: boolean
61
+ remediation?: {
62
+ toolCall?: { name: string; args: Record<string, unknown> }
63
+ userQuestion?: { code: string; question: string }
64
+ docsUrl?: string
65
+ }
66
+ /** Precedence rule for an LLM agent that sees BOTH toolCall AND userQuestion:
67
+ * - toolCall + userQuestion together = userQuestion wins (the human decides).
68
+ * - toolCall alone = agent proceeds.
69
+ * - userQuestion alone = agent stops and asks.
70
+ * No silent ambiguity. */
71
+ details?: unknown
72
+ }
73
+
74
+ export type ToolResponse<T> = { ok: true; data: T } | { ok: false; error: ToolError }
75
+
76
+ /** Unsigned Soroban transaction envelope, base64-encoded XDR (Soroban SDK convention).
77
+ * Pinned for CLI + MCP + WalletAdapter compatibility. Hex / raw-bytes / TS objects
78
+ * are NOT accepted - the contract is base64 string, full stop. */
79
+ export type UnsignedXdrB64 = string
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ export * from './adapters/oz/index.ts'
2
+ export * from './errors.ts'
3
+ export * from './ir/index.ts'
4
+ export * from './mandate/index.ts'
5
+ export * from './record/index.ts'
6
+ export * from './registry/index.ts'
7
+ export * from './seams/index.ts'
8
+ export * from './synth/index.ts'
9
+ export * from './types.ts'
@@ -0,0 +1,13 @@
1
+ // src/ir/index.ts - re-export the PolicyIR ("Policy Tree") types.
2
+
3
+ export type {
4
+ IRCompare,
5
+ IRCompOp,
6
+ IRCondition,
7
+ IRLogic,
8
+ IRPolicyRule,
9
+ IRScalarType,
10
+ IRSelector,
11
+ IRVecMode,
12
+ PolicyIR,
13
+ } from './types.ts'
@@ -0,0 +1,94 @@
1
+ // src/ir/types.ts - the PolicyIR ("Policy Tree").
2
+ //
3
+ // `PolicyIR` is the custody-agnostic "Policy Tree" hub (the diagram's Policy
4
+ // Tree) that every CustodyAdapter compiles FROM. It generalizes the NEAR-V2
5
+ // policy schema (roles / scope filter / guard / constraint / comparison leaves /
6
+ // default behaviour) into one chain-neutral shape and extends it with the
7
+ // OZ-only selectors (spend window, oracle price, invocation count, time) so a
8
+ // single IR can serve every backend. NEAR-V2 has no stateful spend window,
9
+ // oracle, time/expiry, or invocation-count; those are marked as OZ extensions
10
+ // below and are only lowered by adapters that declare support for them.
11
+
12
+ /** Comparison operator. Mirrors NEAR-V2 `CompOp`. */
13
+ export type IRCompOp = 'eq' | 'lt' | 'lte' | 'gt' | 'gte'
14
+
15
+ /** Boolean combinator. Mirrors NEAR-V2 `LogicOp`. */
16
+ export type IRLogic = 'and' | 'or'
17
+
18
+ /** Vector match mode for a repeated arg. Mirrors NEAR-V2 `VecMode`. */
19
+ export type IRVecMode = 'all' | 'any'
20
+
21
+ /** Scalar value type. Mirrors NEAR-V2 `ScValType`, superset for OZ. */
22
+ export type IRScalarType =
23
+ | 'address'
24
+ | 'i128'
25
+ | 'u128'
26
+ | 'u32'
27
+ | 'u64'
28
+ | 'i64'
29
+ | 'symbol'
30
+ | 'bytes'
31
+ | 'bool'
32
+
33
+ /** WHERE in the authorized call a value is read. NEAR-V2 selectors first, OZ
34
+ * extensions after (an adapter that cannot express an extension flags it). */
35
+ export type IRSelector =
36
+ // --- NEAR-V2 selectors ---
37
+ | {
38
+ kind: 'arg'
39
+ argIndex: number
40
+ fieldIndex?: number
41
+ vecMode?: IRVecMode
42
+ scalarType: IRScalarType
43
+ } // Stellar StellarComp
44
+ | { kind: 'calldata'; offset: number; length: number } // EVM ByteComp
45
+ | { kind: 'value' } // EVM tx.value
46
+ // --- OZ extensions (not in NEAR-V2; lowered only by adapters that support them) ---
47
+ | { kind: 'amount'; token: string } // token amount this call moves
48
+ | { kind: 'window_spent'; token: string; windowSeconds: number } // stateful cumulative spend
49
+ | { kind: 'invocation_count'; windowSeconds: number }
50
+ | { kind: 'now' }
51
+ | { kind: 'valid_until' }
52
+ | { kind: 'oracle_price'; asset: string }
53
+
54
+ /** A single comparison leaf. `value` is a decimal/hex string (i128-safe;
55
+ * never a JS number). */
56
+ export interface IRCompare {
57
+ selector: IRSelector
58
+ operator: IRCompOp
59
+ value: string
60
+ }
61
+
62
+ /** Condition tree. NEAR-V2 guard/constraint are flat And/Or; the IR allows
63
+ * nesting + `not` + `in` so the same IR can later lower to the OZ predicate
64
+ * DSL. Week-1 adapters only lower the flat supported subset and flag the rest. */
65
+ export type IRCondition =
66
+ | { op: 'and' | 'or'; children: IRCondition[] }
67
+ | { op: 'not'; child: IRCondition }
68
+ | { op: 'compare'; compare: IRCompare }
69
+ | { op: 'in'; selector: IRSelector; values: string[] }
70
+
71
+ export interface IRPolicyRule {
72
+ /** NEAR-V2 roles whitelist (empty = any; owner exempt). */
73
+ roles: string[]
74
+ /** NEAR-V2 scope filter; each field optional (absent = wildcard). */
75
+ scope: { chainId?: number; contract?: string; method?: string }
76
+ /** NEAR-V2 guard (applicability; skip the rule when it fails). */
77
+ guard?: IRCondition
78
+ /** NEAR-V2 constraint (AND; reject the transaction when any fails). */
79
+ constraints: IRCondition[]
80
+ // --- OZ extensions ---
81
+ /** M-of-N approval (OZ threshold primitives). */
82
+ approval?: { kind: 'threshold'; threshold: number; weights?: Record<string, number> }
83
+ /** OZ context-rule expiry. */
84
+ expiry?: { validUntilLedger?: number; validUntilUnixSeconds?: number }
85
+ }
86
+
87
+ export interface PolicyIR {
88
+ /** NEAR-V2 PolicyChain. */
89
+ chain: 'stellar' | 'evm'
90
+ /** NEAR-V2 DefaultBehavior. OZ context rules are deny-by-default, so
91
+ * `deny_all` is the OZ default fallback when no rule matches. */
92
+ defaultBehavior: 'allow_all' | 'deny_all'
93
+ rules: IRPolicyRule[]
94
+ }
@@ -0,0 +1,4 @@
1
+ // src/mandate/index.ts - re-export the Mandate source.
2
+
3
+ export { mandateToPolicyIR } from './to-ir.ts'
4
+ export type { MandateSpec } from './types.ts'
@@ -0,0 +1,71 @@
1
+ // src/mandate/to-ir.ts - deterministic lowering of a MandateSpec to a PolicyIR.
2
+ //
3
+ // `mandateToPolicyIR` is pure and total: the same spec always lowers to a
4
+ // byte-identical PolicyIR. No decoding, no inference, no clock. Each mandate
5
+ // field maps to exactly one IR construct:
6
+ // contract/method -> rule.scope
7
+ // spendingLimit -> a `window_spent(token,window) <= limit` compare
8
+ // approvalThreshold-> rule.approval.threshold
9
+ // recipients -> an `in` condition on the recipient arg (Path-B flagged
10
+ // by the OZ adapter; that is expected)
11
+ // expiry -> rule.expiry
12
+ // The top-level default is `deny_all` (OZ context rules are deny-by-default).
13
+
14
+ import type { IRCondition, IRPolicyRule, IRSelector, PolicyIR } from '../ir/types.ts'
15
+ import type { MandateSpec } from './types.ts'
16
+
17
+ /** Arg index the recipient allowlist constrains. Pinned to the SEP-41
18
+ * `transfer(from, to, amount)` convention where `to` is arg 1. This condition
19
+ * is flagged Path-B by the OZ adapter (no built-in primitive expresses an arg
20
+ * allowlist), so the index only needs to be deterministic in week-1. */
21
+ const RECIPIENT_ARG_INDEX = 1
22
+
23
+ export function mandateToPolicyIR(spec: MandateSpec): PolicyIR {
24
+ const scope: IRPolicyRule['scope'] = { contract: spec.contract }
25
+ if (spec.method !== undefined) scope.method = spec.method
26
+
27
+ const constraints: IRCondition[] = []
28
+
29
+ if (spec.spendingLimit) {
30
+ constraints.push({
31
+ op: 'compare',
32
+ compare: {
33
+ selector: {
34
+ kind: 'window_spent',
35
+ token: spec.spendingLimit.token,
36
+ windowSeconds: spec.spendingLimit.windowSeconds,
37
+ },
38
+ operator: 'lte',
39
+ value: spec.spendingLimit.limit,
40
+ },
41
+ })
42
+ }
43
+
44
+ if (spec.recipients && spec.recipients.length > 0) {
45
+ const selector: IRSelector = {
46
+ kind: 'arg',
47
+ argIndex: RECIPIENT_ARG_INDEX,
48
+ scalarType: 'address',
49
+ }
50
+ constraints.push({ op: 'in', selector, values: [...spec.recipients] })
51
+ }
52
+
53
+ const rule: IRPolicyRule = { roles: [], scope, constraints }
54
+
55
+ if (spec.approvalThreshold !== undefined) {
56
+ rule.approval = { kind: 'threshold', threshold: spec.approvalThreshold }
57
+ }
58
+
59
+ if (spec.expiry) {
60
+ const expiry: NonNullable<IRPolicyRule['expiry']> = {}
61
+ if (spec.expiry.validUntilLedger !== undefined) {
62
+ expiry.validUntilLedger = spec.expiry.validUntilLedger
63
+ }
64
+ if (spec.expiry.validUntilUnixSeconds !== undefined) {
65
+ expiry.validUntilUnixSeconds = spec.expiry.validUntilUnixSeconds
66
+ }
67
+ rule.expiry = expiry
68
+ }
69
+
70
+ return { chain: spec.chain, defaultBehavior: 'deny_all', rules: [rule] }
71
+ }
@@ -0,0 +1,21 @@
1
+ // src/mandate/types.ts - the deterministic Mandate source.
2
+ //
3
+ // A MandateSpec is a declarative policy statement: "this account may call this
4
+ // contract, spending at most L per window, requiring M approvals, to these
5
+ // recipients, until this expiry". It carries NO transaction to decode, NO
6
+ // parseConfidence, and NO inference - it lowers deterministically to a
7
+ // PolicyIR. It is the clean end-to-end demo path, co-equal with the recorder.
8
+
9
+ export interface MandateSpec {
10
+ chain: 'stellar'
11
+ contract: string
12
+ method?: string
13
+ /** -> OZ `spending_limit` primitive. `limit` is an i128 decimal string. */
14
+ spendingLimit?: { token: string; limit: string; windowSeconds: number }
15
+ /** -> OZ `simple_threshold` / `weighted_threshold` primitive. */
16
+ approvalThreshold?: number
17
+ /** Recipient allowlist -> an `in` arg condition (flagged Path-B in week-1). */
18
+ recipients?: string[]
19
+ /** -> OZ context-rule expiry. */
20
+ expiry?: { validUntilLedger?: number; validUntilUnixSeconds?: number }
21
+ }