fix: say so when a session has no approval channel to ask through
Release / release (push) Successful in 6s
Release / release (push) Successful in 6s
`ask` mode promises that an out-of-facet call is put to the user, and that promise needs a channel. A session can be composed without one: the `danger-full-access` permission preset sets `approval: never`, and the approval service's `decide()` then returns "rejected" before consulting any answerer. The call is refused, `facet action=add` is refused with it — widening is itself a question — and the scope can never be widened. The model is told none of that. `serviceAsk` replaces a rejected ask with `the user rejected tool \"…\"`, discarding the reason this guard wrote, so the binding is the only channel left that can state what is true. It now reads the session's effective approval policy and, when there is no channel, says the call was refused rather than asked and names the mechanism that still works: `extraRoots` is configuration applied by the profile, not a grant asked for per conversation. A missing approval service counts as no channel (the tool pipeline denies an ask it cannot route); an unreadable policy counts as a channel, so a transient failure never tells a user to go and edit their configuration.
This commit is contained in:
1 parent
6e696f10a2
commit
a1c8ecb1e8
4 files changed
+120
-6
No files matched your search
+36
-1
@@ -71,6 +71,35 @@ export function apply(ctx, config) {
|
||||
// the session was loaded with, for the life of the session.
|
||||
const modeNow = () => policy.mode
|
||||
|
||||
/**
|
||||
* Whether an approval request from this session can reach a person.
|
||||
*
|
||||
* `ask` mode is a promise that an out-of-facet call is *put to the user*, and
|
||||
* that promise needs a channel. A session can be composed without one: the
|
||||
* `danger-full-access` permission preset sets `approval: never`, under which
|
||||
* the approval service's `decide()` returns "rejected" before consulting any
|
||||
* answerer. An ask is then a denial, and `facet action=add` — itself an ask —
|
||||
* is denied with it, so the scope can never be widened while the model is told
|
||||
* only `the user rejected tool "…"`. Naming that is the binding's job; see
|
||||
* `facetContextText`.
|
||||
*
|
||||
* A missing service is a definite no (the tool pipeline denies an ask it
|
||||
* cannot route). An unreadable policy is treated as a yes, so a transient
|
||||
* failure does not tell a user to go and edit their configuration.
|
||||
*
|
||||
* @param {{session?: object}|undefined} agent - the agent whose session supplies the policy.
|
||||
* @returns {boolean} whether an ask can reach a person.
|
||||
*/
|
||||
const canAsk = (agent) => {
|
||||
const approval = ctx.get('approval')
|
||||
if (approval === undefined || agent?.session === undefined) return false
|
||||
try {
|
||||
return approval.effectivePolicy(agent.session) !== 'never'
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// `off` is the one part that stays a mount-time decision. Deciding it here
|
||||
// means an `off` deployment contributes no rule, no binding and no tool —
|
||||
// which is the point of the mode. The cost is that turning it *on* later needs
|
||||
@@ -187,7 +216,13 @@ export function apply(ctx, config) {
|
||||
text: (context) => {
|
||||
const scope = policy.scopeFor(context?.agent)
|
||||
if (scope === undefined) return ''
|
||||
return facetContextText({ cwd: scope.cwd, root: scope.root, extras: scope.extras, mode: modeNow() })
|
||||
return facetContextText({
|
||||
cwd: scope.cwd,
|
||||
root: scope.root,
|
||||
extras: scope.extras,
|
||||
mode: modeNow(),
|
||||
canAsk: canAsk(context?.agent),
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
Reference in new issue
Block a user