Skip to content

feat(policy): add RulePolicy, the rule-shaped definition as a TransactionPolicy - #89

Draft
AlonzoRicardo wants to merge 1 commit into
feat/policy-transaction-policy-basefrom
feat/policy-rule-policy
Draft

AlonzoRicardo wants to merge 1 commit into
feat/policy-transaction-policy-basefrom
feat/policy-rule-policy

Conversation

@AlonzoRicardo

Copy link
Copy Markdown
Contributor

What

RulePolicy: a rule-shaped policy definition (the object registerPolicy accepts today) implemented as a TransactionPolicy. Stacked on the base-classes PR; nothing is wired into the proxy yet.

This is the adapter that lets rule-shaped and code-defined policies compose under one contract, and it is deliberately built out of the engine's own parts — validatePolicy, PolicyRegistry, buildContext, evaluate — so that within a single policy the semantics are identical to today's engine rather than a reimplementation of them.

Verdict mapping

Evaluator outcome RulePolicy.evaluate
ALLOW (reason matched) { outcome: 'allow' }
ALLOW (reason override) { outcome: 'allow', reason: 'override' }
BLOCK from a matching DENY rule { outcome: 'deny', reason } (rule reason, or name, or the condition-error/timeout message)
BLOCK because rules addressed the operation but no conditions held { outcome: 'abstain' }

Within one policy, "my rules addressed it but none fired" is no opinion; the engine's cross-policy default-deny then produces the same final decision today's engine does. Cross-policy composition (DENY-wins across policies, account-scope override of project DENYs) stays in the engine.

match honours the definition's wallet and account bindings (path and index) as well as the rules' operation fields, so an instance is safe to consult even unfiltered. commit / rollback are no-ops; state and onSuccess remain reserved.

What this PR surfaced

Writing RulePolicy showed that OperationRaw needed path and index alongside the account view — read-only views don't expose either, and account-scope bindings match on them. That was folded into the base-classes PR before it was committed, which is exactly why this step runs before the engine switch.

DEFAULT_CONDITION_TIMEOUT_MS moved from a module-private constant in policy-engine.js to constants.js so the engine and RulePolicy read one value. No behaviour change.

Tests

tests/rule-policy.test.js (25 tests): construction reuses registerPolicy validation; match on literal / wildcard / array operations and on wallet, path and index bindings; evaluate for ALLOW, DENY with and without reason, DENY-wins within a policy, throwing conditions (fail-closed on DENY, open-as-no-match on ALLOW), timeouts, the frozen snapshotted PolicyContext conditions receive, and override_broader_scope; plus a table-driven equivalence check that runs the same definition and record through evaluate() directly and asserts the mapping above.

https://claude.ai/code/session_01EU4Ntwfs7hKAB9mmdyxYtC

…tionPolicy

Bridges rule-shaped policy definitions (the object registerPolicy
accepts) onto the TransactionPolicy contract so both kinds of policy can
compose under one engine path. Built from the engine's own parts —
validatePolicy, PolicyRegistry, buildContext, evaluate — so within a
single policy the semantics are identical to today's, not reimplemented.

Verdict mapping from the evaluator: ALLOW -> allow (reason 'override'
preserved), a matching DENY rule -> deny with the rule's reason, rules
that addressed the operation but whose conditions did not hold ->
abstain. Within one policy that is "no opinion"; the engine's
cross-policy default-deny then yields the same decision as today.

match() honours wallet, path and index bindings as well as rule
operations, so an instance is safe to consult unfiltered. commit and
rollback are no-ops; state and onSuccess remain reserved.

DEFAULT_CONDITION_TIMEOUT_MS moves from policy-engine.js to constants.js
so the engine and RulePolicy read one value. No behaviour change.

Tests assert behaviour directly and, table-driven, verdict equivalence
against evaluate() on the same definition and record.

Claude-Session: https://claude.ai/code/session_01EU4Ntwfs7hKAB9mmdyxYtC
@AlonzoRicardo
AlonzoRicardo force-pushed the feat/policy-transaction-policy-base branch from 4e8bd4e to 8ba432f Compare September 7, 2026 20:09
@AlonzoRicardo
AlonzoRicardo force-pushed the feat/policy-rule-policy branch from 846ed0f to 44f9f8a Compare September 7, 2026 20:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants