Summary
Add a host-only query that answers "may caller path X call target Y with these args", running the full call-gate semantics. It is the call-side counterpart of api.slothlet.event.resolveLevel(subscriberPath, event), which was added for slothlet-vine's event forwarding.
Why
@cldmv/slothlet-vine needs to gate served calls as a channel principal (CLDMV/slothlet-vine#33): a far peer over an untrusted transport (a browser over a WebSocket) must be judged by the serving instance's rules, not by the far side's own stub. Slothlet has no way to dispatch a call as an arbitrary identity (context.run keeps host standing, lockCaller is a passthrough from the host), so the vine has to ask the rules engine and enforce the answer itself.
The only public query today, permissions.global.checkAccess(caller, target), is a silent query that passes callMeta = null. Through it:
- resource-scoped conditions (
condition: (ctx, { args }) => …) receive null and are non-matches;
- 3.21 principals (
requires: [...]) that are stale are non-matches instead of being resolved.
So a rule like { caller: "remote.renderer", target: "project.files.list", condition: (ctx, meta) => meta?.args?.[0] === ctx.actor?.project } stops meaning what it says when it's evaluated through checkAccess. The error goes whichever way defaultPolicy points.
Proposal
api.slothlet.permissions.global.checkCall(callerPath, targetPath, args) → boolean | Promise<boolean>
- Forwards
callMeta = { args, target: targetPath } to function conditions, exactly as the call gate builds it in enforcePermission.
- Evaluates conditions against the ambient runtime context (
tryGetContext()?.context), like checkAccess / resolveLevel.
- When a
requires principal is stale, resolves it and re-evaluates, mirroring the wrapper's deferral. Returns a Promise only in that case; the sync fast path stays sync.
- Treats the caller as a module with no source file: the self-call bypass never applies, and a module-private target is denied (not judged by the
private.host policy).
- Emits the audit lifecycle events (
permission:denied / permission:allowed / permission:default) so a probing peer is visible like any other denial.
- Returns
true when the permission system is disabled, consistent with checkAccess.
- Host-only: modules can't call it, like
event.resolveLevel.
Consumer
slothlet-vine #33 (serve-side principal gate), followed by CLDMV/slothlet-vine#51 (serve around). rummage's WebSocket serve (CLDMV/rummage#69) needs this before it can widen its served surface past read-only host.**.
Summary
Add a host-only query that answers "may caller path X call target Y with these args", running the full call-gate semantics. It is the call-side counterpart of
api.slothlet.event.resolveLevel(subscriberPath, event), which was added for slothlet-vine's event forwarding.Why
@cldmv/slothlet-vineneeds to gate served calls as a channel principal (CLDMV/slothlet-vine#33): a far peer over an untrusted transport (a browser over a WebSocket) must be judged by the serving instance's rules, not by the far side's own stub. Slothlet has no way to dispatch a call as an arbitrary identity (context.runkeeps host standing,lockCalleris a passthrough from the host), so the vine has to ask the rules engine and enforce the answer itself.The only public query today,
permissions.global.checkAccess(caller, target), is a silent query that passescallMeta = null. Through it:condition: (ctx, { args }) => …) receivenulland are non-matches;requires: [...]) that are stale are non-matches instead of being resolved.So a rule like
{ caller: "remote.renderer", target: "project.files.list", condition: (ctx, meta) => meta?.args?.[0] === ctx.actor?.project }stops meaning what it says when it's evaluated throughcheckAccess. The error goes whichever waydefaultPolicypoints.Proposal
api.slothlet.permissions.global.checkCall(callerPath, targetPath, args) → boolean | Promise<boolean>callMeta = { args, target: targetPath }to function conditions, exactly as the call gate builds it inenforcePermission.tryGetContext()?.context), likecheckAccess/resolveLevel.requiresprincipal is stale, resolves it and re-evaluates, mirroring the wrapper's deferral. Returns a Promise only in that case; the sync fast path stays sync.private.hostpolicy).permission:denied/permission:allowed/permission:default) so a probing peer is visible like any other denial.truewhen the permission system is disabled, consistent withcheckAccess.event.resolveLevel.Consumer
slothlet-vine #33 (serve-side principal gate), followed by CLDMV/slothlet-vine#51 (
servearound). rummage's WebSocket serve (CLDMV/rummage#69) needs this before it can widen its served surface past read-onlyhost.**.