PCL Composite Policies — LogicalPolicy and ForEachPolicy
Structural combinators that compose leaf templates into AND / OR trees or fan a single rule over resolved agent owners.
LogicalPolicy and ForEachPolicy are structural policy templates that wrap other PolicySet entries into a tree instead of carrying their own leaf rule. LogicalPolicy combines its children under an AND (Every) or OR (Any) quantifier; ForEachPolicy takes a single child and applies it once per resolved subject — currently the caller's set of agent owners. Both are subject to a maximum structural depth (MaxDepthExceeded) and to a set of validation errors (LogicalPolicyChildrenEmpty, ForEachChildAbsent, QuantifierUnspecified, ChildSelectorNotEmpty) that reject malformed trees at registration.
The structural structs
Straight from
IPcl.sol:enum LogicalQuantifier { Unspecified, And, Or }
enum ForEachQuantifier { Unspecified, Any, Every }
enum ForEachSubject { Unspecified, AgentOwners }
struct LogicalPolicy {
LogicalQuantifier quantifier;
PolicySet[] children;
}
struct ForEachPolicy {
ForEachQuantifier quantifier;
ForEachSubject subject;
PolicySet child;
} LogicalPolicy — AND / OR combinator
LogicalPolicy collects two or more PolicySet children and evaluates them under one quantifier:And— every child must pass. On the first failure PCL surfaces that child's ReasonCode.Or— at least one child must pass. If every child fails, PCL surfacesAnyOfRejected(bytes[] childReverts)carrying the ABI-encoded revert payload of each failed child so the client can unwrap them and pick a user-facing message.
Each child
PolicySet inside a LogicalPolicy must have an empty selector — the selector is set once on the enclosing PolicySet. Registering a child with a non-empty selector reverts with ChildSelectorNotEmpty. An empty children array is rejected with LogicalPolicyChildrenEmpty, and a nil entry within the array reverts with LogicalPolicyChildNil(uint256 index). Any leaf template can appear as a child; there is no runtime restriction on which templates may be nested.ForEachPolicy — fan a rule over a subject set
ForEachPolicy applies its single child policy once per element of a resolved subject set. The only subject implemented today is AgentOwners, which expands to the set of on-chain owners of every agent bound to the caller wallet. The quantifier controls how the per-subject results combine: Every requires all owners to pass, Any requires at least one. Common use case: "at least one of this agent's owners must be attested" — wrap EAS_POLICY in a ForEachPolicy(subject: AgentOwners, quantifier: Any).Registration validates the tree eagerly: an unset subject reverts with
ForEachSubjectUnspecified, an unset quantifier with QuantifierUnspecified, and an absent child with ForEachChildAbsent. An unrecognised subject enum value reverts with UnknownForEachSubject(uint8 subject).// "At least one of this caller's agent owners must hold the KYC attestation."
EasPolicy memory eas = EasPolicy({
easContract: 0x1000000000000000000000000000000000000007,
indexContract: 0x1000000000000000000000000000000000000008,
schemaUid: 0x5f3a2b0e1d5c9a7f6e3b4a1d2c8f7b6a5e4d3c2b1a0f9e8d7c6b5a4938271605
});
PolicySet memory child = PolicySet({
templateId: "EAS_POLICY",
policy: abi.encode(eas),
selector: "" // required — must be empty on structural children
});
ForEachPolicy memory forOwners = ForEachPolicy({
quantifier: ForEachQuantifier.Any,
subject: ForEachSubject.AgentOwners,
child: child
}); Depth limit and diagnostics
Structural policies count against a fixed maximum depth. Exceeding it during registration reverts with
MaxDepthExceeded(uint8 maxDepth) — nesting an OR inside an AND inside another OR quickly hits the ceiling. When an Or combinator rejects, the revert data is AnyOfRejected(bytes[] childReverts): iterate childReverts, decodeErrorResult each entry, and decide which one is worth showing to the user. All other structural errors (LogicalPolicyChildrenEmpty, LogicalPolicyChildNil, ForEachChildAbsent, ForEachSubjectUnspecified, QuantifierUnspecified, UnknownForEachSubject, ChildSelectorNotEmpty) are surfaced at changeContractPolicies / setGlobalPolicies time, so a well-formed config cannot fail these at execution.