You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
sheaves: guardCoversPoint, getGuardAt, and collectSheafGuard read rest guards per argument #1150
Priority: low. This fails closed. A call that gets through still reaches a section's own exo, which enforces its guard correctly, so no authority escapes. But the sheaf refuses calls its sections admit, advertises calls none of them admit, and chooses sections from a wrong reading of their guards.
The rule
Exo checks a call by matching the whole argument array against M.splitArray(argGuards, optionalArgGuards, restArgGuard) (buildMatchConfig in @endo/exo). The rest guard therefore matches the array of trailing arguments, not each argument. "Any number of trailing strings" is .rest(M.arrayOf(M.string())). .rest(M.string()) refuses every call, even one with no trailing arguments, because [] is not a string.
Where sheaves reads it per argument
guardCoversPoint (packages/sheaves/src/match.ts) checks each trailing argument against the rest guard:
For a section guarded as log: M.call(M.string()).rest(M.arrayOf(M.string())), exo admits log('a', 'b'), but guardCoversPoint leaves the section out.
For a section guarded as .rest(M.string()), guardCoversPoint selects it for log('a', 'b'), and its exo rejects the call, as it rejects every call.
getGuardAt (packages/kernel-utils/src/guard-algebra.ts) returns the rest guard as the guard for a single position past the fixed arity.
collectSheafGuard (packages/sheaves/src/guard.ts) builds per-position unions from getGuardAt. With section A guarded as (string).rest(M.arrayOf(M.number())) and section B guarded as (string, string), position 1 becomes an optional M.or(M.arrayOf(M.number()), M.string()). The sheaf's guard then:
admits ('s', [1, 2]), which neither section admits;
refuses ('s', 1, 2), which A admits.
Separately from the rest misreading, ORing position by position admits cross-combinations across sections, such as A's argument 0 with B's argument 1. join had the same flaw, fixed in #1134 by rendering one M.splitArray per row.
Suggested fix
Add a kernel-utils helper that checks a call against a method guard as exo does: match harden(args) against M.splitArray(argGuards, optionals, restArgGuard), and without a rest guard also cap the argument count. Use it in guardCoversPoint.
Have collectSheafGuard render a method whose sections differ as M.callWhen().rest(M.or(M.splitArray(...), …)), one per section, which is exactly their union, as join does in feat(kernel-utils): attenuation by narrowing #1134. Then getGuardAt needs no rest branch: it can return undefined past the fixed arity, or be removed. It is unreleased.
In guardCoversPoint, use Object.hasOwn rather than method in methodGuards, which matches inherited names such as toString, and admit an explicit undefined at an optional position, as exo does.
Rewrite the tests that assert matches(value, restArgGuard) against single values (sheaves/src/guard.test.ts, kernel-utils/src/guard-algebra.test.ts) as call-level checks.
Priority: low. This fails closed. A call that gets through still reaches a section's own exo, which enforces its guard correctly, so no authority escapes. But the sheaf refuses calls its sections admit, advertises calls none of them admit, and chooses sections from a wrong reading of their guards.
The rule
Exo checks a call by matching the whole argument array against
M.splitArray(argGuards, optionalArgGuards, restArgGuard)(buildMatchConfigin@endo/exo). The rest guard therefore matches the array of trailing arguments, not each argument. "Any number of trailing strings" is.rest(M.arrayOf(M.string()))..rest(M.string())refuses every call, even one with no trailing arguments, because[]is not a string.Where sheaves reads it per argument
guardCoversPoint(packages/sheaves/src/match.ts) checks each trailing argument against the rest guard:log: M.call(M.string()).rest(M.arrayOf(M.string())), exo admitslog('a', 'b'), butguardCoversPointleaves the section out..rest(M.string()),guardCoversPointselects it forlog('a', 'b'), and its exo rejects the call, as it rejects every call.getGuardAt(packages/kernel-utils/src/guard-algebra.ts) returns the rest guard as the guard for a single position past the fixed arity.collectSheafGuard(packages/sheaves/src/guard.ts) builds per-position unions fromgetGuardAt. With section A guarded as(string).rest(M.arrayOf(M.number()))and section B guarded as(string, string), position 1 becomes an optionalM.or(M.arrayOf(M.number()), M.string()). The sheaf's guard then:('s', [1, 2]), which neither section admits;('s', 1, 2), which A admits.Separately from the rest misreading, ORing position by position admits cross-combinations across sections, such as A's argument 0 with B's argument 1.
joinhad the same flaw, fixed in #1134 by rendering oneM.splitArrayper row.Suggested fix
harden(args)againstM.splitArray(argGuards, optionals, restArgGuard), and without a rest guard also cap the argument count. Use it inguardCoversPoint.collectSheafGuardrender a method whose sections differ asM.callWhen().rest(M.or(M.splitArray(...), …)), one per section, which is exactly their union, asjoindoes in feat(kernel-utils): attenuation by narrowing #1134. ThengetGuardAtneeds no rest branch: it can returnundefinedpast the fixed arity, or be removed. It is unreleased.guardCoversPoint, useObject.hasOwnrather thanmethod in methodGuards, which matches inherited names such astoString, and admit an explicitundefinedat an optional position, as exo does.matches(value, restArgGuard)against single values (sheaves/src/guard.test.ts,kernel-utils/src/guard-algebra.test.ts) as call-level checks.