Context
The policy engine splits cleanly into a portable core and an Antelope-flavoured vocabulary, but the two are currently fused:
Portable as-is — document skeleton (schemaVersion, default: "deny", chain{name,chainId}, rules[], maxActionsPerTransaction), RuleLimits, match operators (lte/gte/eq/in/notIn), JCS canonicalization + sha256 (src/core/canonical/jcs.ts), the verifyStoredPolicy gate (src/core/policy/onchain.ts), PolicyCache anti-rollback, QuotaJournal, and the pure evaluatePolicy control flow (deny-wins, first-match allow, fail closed).
Antelope-specific, baked into the core today:
MATCH_PATH_PATTERN in src/core/policy/schema.ts hard-codes authorization.(actor|permission) — the Antelope auth model.
chainId pattern ^[0-9a-f]{64}$ (Antelope 64-hex).
- Asset grammar
"1.0000 XPR" in src/core/asset.ts ([A-Z]{1,7} symbol, int64 bound).
- Engine conventions in
src/core/policy/engine.ts: actionAsset() reads data.quantity, quota-recipient logic reads data.to — eosio.token shapes.
- The only provider is
xpr.rpc.tableRow, resolved through Antelope get_table_rows (src/daemon/providerResolver.ts).
- Policy storage is the AssemblyScript
signbox contract (contract/assembly/), keyed on Antelope Name (agent.N).
Coupling debt that multiplies per chain: the web editor hand-duplicates the schema — regex copies in web/src/editor/compile.ts, field sets in decompile.ts, plus a second canonicalize implementation. Parity is only enforced at the boundary (the daemon rejects not_canonical), not by construction.
Proposed design
Hard invariant: the document skeleton, JCS canonicalization, sha256 policyhash, limits model, and the fail-closed pipeline stay chain-agnostic and byte-identical in behaviour on every chain. Only the vocabulary varies.
PolicyDialect (a member of ChainModule — see the chain-modules issue):
interface PolicyDialect {
matchPathPattern: RegExp; // per-chain closed match vocabulary
resolveAuthPath(action, path): unknown; // authorization.actor vs SUI sender, etc.
asset: { parse(s): AssetAmount; format(a): string }; // per-chain grammar/bounds
amountPath: string; // which data path is "the amount" (quotas)
recipientPath: string; // which data path is "the recipient" (cooldowns)
providers: { namespace: string; schema: JSONSchema; resolver: ProviderResolver };
}
- The core schema validates the skeleton; the dialect validates the chain parts.
evaluatePolicy receives the dialect instead of hardcoding actionAsset/data.to.
- Per-chain policy registry contract (decided): each chain deploys its own registry implementing the same logical contract — row
(agent, authority, version, policyhash, policyjson, enabled), monotonic version (anti-rollback), integrity sha256(policyjson) == policyhash, authority-only writes, kill-switch. verifyStoredPolicy (JCS + hash + schema) remains the single chain-agnostic gate on the daemon side.
- Shared
policy-core package (schema + canonicalize + dialect descriptors as data) consumed by BOTH the daemon and web/ — the editor's hand-kept regex/field-set/canonicalize copies disappear, and the editor learns a new chain by loading its dialect descriptor instead of a code fork.
Tasks
Phase A — dialect extraction (XPR only, no behavior change)
Phase B — follow-up passes (separate issues when Phase A lands)
- SUI policy registry contract (Move shared object) + SUI dialect.
- Metal policy registry contract (EVM/Solidity) + Metal dialect.
Invariants to preserve
INV-001 (deny by default), INV-004 (policy not agent-modifiable), INV-008-A (providers deterministic & narrowing-only), INV-010 (fail closed), INV-013 (explicit chain identity — a policy stays bound to one chainId).
Acceptance criteria
- XPR policies (bytes, hashes, decisions) are unchanged after the dialect extraction — existing on-chain policies keep verifying.
- Daemon and web editor validate/canonicalize through the same package (no hand-kept copies left).
- A new chain's policy support = one dialect descriptor + one registry contract, zero core-engine edits.
🤖 Generated with Claude Code
https://claude.ai/code/session_01RJ7hqFnC9Nnyf2Db2h3WTb
Context
The policy engine splits cleanly into a portable core and an Antelope-flavoured vocabulary, but the two are currently fused:
Portable as-is — document skeleton (
schemaVersion,default: "deny",chain{name,chainId},rules[],maxActionsPerTransaction),RuleLimits, match operators (lte/gte/eq/in/notIn), JCS canonicalization + sha256 (src/core/canonical/jcs.ts), theverifyStoredPolicygate (src/core/policy/onchain.ts),PolicyCacheanti-rollback,QuotaJournal, and the pureevaluatePolicycontrol flow (deny-wins, first-match allow, fail closed).Antelope-specific, baked into the core today:
MATCH_PATH_PATTERNinsrc/core/policy/schema.tshard-codesauthorization.(actor|permission)— the Antelope auth model.chainIdpattern^[0-9a-f]{64}$(Antelope 64-hex)."1.0000 XPR"insrc/core/asset.ts([A-Z]{1,7}symbol, int64 bound).src/core/policy/engine.ts:actionAsset()readsdata.quantity, quota-recipient logic readsdata.to—eosio.tokenshapes.xpr.rpc.tableRow, resolved through Antelopeget_table_rows(src/daemon/providerResolver.ts).signboxcontract (contract/assembly/), keyed on AntelopeName(agent.N).Coupling debt that multiplies per chain: the web editor hand-duplicates the schema — regex copies in
web/src/editor/compile.ts, field sets indecompile.ts, plus a secondcanonicalizeimplementation. Parity is only enforced at the boundary (the daemon rejectsnot_canonical), not by construction.Proposed design
Hard invariant: the document skeleton, JCS canonicalization, sha256 policyhash, limits model, and the fail-closed pipeline stay chain-agnostic and byte-identical in behaviour on every chain. Only the vocabulary varies.
PolicyDialect(a member ofChainModule— see the chain-modules issue):evaluatePolicyreceives the dialect instead of hardcodingactionAsset/data.to.(agent, authority, version, policyhash, policyjson, enabled), monotonic version (anti-rollback), integritysha256(policyjson) == policyhash, authority-only writes, kill-switch.verifyStoredPolicy(JCS + hash + schema) remains the single chain-agnostic gate on the daemon side.policy-corepackage (schema + canonicalize + dialect descriptors as data) consumed by BOTH the daemon andweb/— the editor's hand-kept regex/field-set/canonicalize copies disappear, and the editor learns a new chain by loading its dialect descriptor instead of a code fork.Tasks
Phase A — dialect extraction (XPR only, no behavior change)
PolicyDialect; extract the Antelope vocabulary fromschema.ts/engine.ts/asset.tsinto the XPR dialect.evaluatePolicy+validatePolicyconsume the dialect (core keeps the skeleton).policy-corepackage; port the web editor onto it (delete the duplicated regexes/canonicalize).Phase B — follow-up passes (separate issues when Phase A lands)
Invariants to preserve
INV-001 (deny by default), INV-004 (policy not agent-modifiable), INV-008-A (providers deterministic & narrowing-only), INV-010 (fail closed), INV-013 (explicit chain identity — a policy stays bound to one chainId).
Acceptance criteria
🤖 Generated with Claude Code
https://claude.ai/code/session_01RJ7hqFnC9Nnyf2Db2h3WTb