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
+17
-3
@@ -175,17 +175,31 @@ export function displayPath(cwd, path) {
|
||||
* @param {string|undefined} input.root - the facet root, or undefined when unscoped.
|
||||
* @param {readonly string[]} input.extras - absolute granted paths, outside the root.
|
||||
* @param {'ask'|'deny'|'off'} input.mode - how an out-of-scope call is handled.
|
||||
* @param {boolean} [input.canAsk] - whether an approval request can reach the user. False means every ask is refused before anyone is consulted.
|
||||
* @returns {string} context text, or `''` to contribute nothing.
|
||||
*/
|
||||
export function facetContextText(input) {
|
||||
const { cwd, root, extras = [], mode } = input
|
||||
const { cwd, root, extras = [], mode, canAsk = true } = input
|
||||
if (mode === 'off' || typeof root !== 'string' || root === '') return ''
|
||||
|
||||
// Stated in terms of what the model should *do*, because the same sentence is
|
||||
// what the user reads in an approval prompt. A denial that does not say how to
|
||||
// proceed is a denial the agent works around with `bash`.
|
||||
const reach =
|
||||
mode === 'deny'
|
||||
//
|
||||
// `canAsk` comes first because it collapses the other two. A session can be
|
||||
// composed without an approval channel — the `danger-full-access` permission
|
||||
// preset sets `approval: never`, and `decide()` then returns "rejected" before
|
||||
// any answerer is consulted. There, an out-of-facet call is refused whatever
|
||||
// `mode` says, and `facet action=add` is refused with it, because widening is
|
||||
// itself a question. Telling the agent to ask would send it into a loop, and
|
||||
// the reason this guard writes never reaches it: `serviceAsk` replaces a
|
||||
// rejected ask with `the user rejected tool "…"`. So the binding is the one
|
||||
// channel that can say what is true, and it names the mechanism that still
|
||||
// works — `extraRoots` is configuration applied by the profile, not a grant
|
||||
// asked for per conversation.
|
||||
const reach = !canAsk
|
||||
? 'refused — approval prompts are off in this session, so `facet action=add` cannot be approved either; only the user can widen this, by adding the path to `extraRoots`'
|
||||
: mode === 'deny'
|
||||
? 'refused — `facet action=add` asks the user to grant a path, and is the only way to widen the scope'
|
||||
: 'put to the user as a question — say in one line what you need and why, then record it with `facet action=add` so the rest of the session does not have to ask again'
|
||||
|
||||
|
||||
Reference in new issue
Block a user