mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
feat(permission): a tighten-only extension's ask is a fresh human approval of the one call
This commit is contained in:
1 parent
2234a3d68b
commit
ec49147dee
12 files changed
+179
-5
No files matched your search
@@ -5,7 +5,7 @@
|
||||
- Protocol ID: `reasonix.extension.v2`
|
||||
- Protocol major: `2`
|
||||
- Schema: `internal/ext/extension/protocol/schema.generated.json`
|
||||
- Schema hash: `sha256:3c01970cdea893303c49150fb2717f592178fb3974a17c91a7848712e516930d`
|
||||
- Schema hash: `sha256:d46a970c1b1ac3a03cbdc9a32e6457e48f02c25a2679a0e3bbc8a727557437e7`
|
||||
|
||||
Within major v2 only optional fields, new enum values, and new methods may
|
||||
be added; existing required fields, directions, limits, error reasons, and
|
||||
|
||||
@@ -280,3 +280,70 @@ func TestEffectAModThatCrashesOrHangsDoesNotFailTheTurn(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A mod's ask puts the call to the person even in a posture that approves
|
||||
// everything, and the answer covers that one call. YOLO does not answer it.
|
||||
func TestEffectAModsAskIsAFreshHumanApprovalThatYoloDoesNotAnswer(t *testing.T) {
|
||||
for _, approve := range []bool{true, false} {
|
||||
name := "declined"
|
||||
if approve {
|
||||
name = "approved"
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
_, workspace, prov := installMod(t, `
|
||||
export function register(on) {
|
||||
on('tool.check', async ($, e, next) => (e.input.command.includes('ASKME') ? { decision: 'ask' } : next(e)));
|
||||
}`)
|
||||
setModsSwitch(t, true)
|
||||
prov.command = "touch asked-" + name + ".txt # ASKME"
|
||||
var mu sync.Mutex
|
||||
var cards []event.Approval
|
||||
sink := event.FuncSink(func(e event.Event) {
|
||||
if e.Kind == event.ApprovalRequest {
|
||||
mu.Lock()
|
||||
cards = append(cards, e.Approval)
|
||||
mu.Unlock()
|
||||
}
|
||||
})
|
||||
res, err := BuildRuntime(t.Context(), Options{Sink: sink})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer res.Controller.Close()
|
||||
res.Controller.EnableInteractiveApproval()
|
||||
res.Controller.SetToolApprovalMode(control.ToolApprovalYolo)
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
done <- res.Controller.Run(ctx, "run it")
|
||||
}()
|
||||
var card event.Approval
|
||||
waitFor(t, "the approval card", func() bool {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
for _, c := range cards {
|
||||
if c.Tool == "mod_check" {
|
||||
card = c
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
if !strings.Contains(card.Subject, "bash") {
|
||||
t.Fatalf("the card does not show the call: %+v", card)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(workspace, "asked-"+name+".txt")); err == nil {
|
||||
t.Fatal("the call ran before the person answered")
|
||||
}
|
||||
res.Controller.Approve(card.ID, approve, false, false)
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("Run: %v", err)
|
||||
}
|
||||
_, statErr := os.Stat(filepath.Join(workspace, "asked-"+name+".txt"))
|
||||
if (statErr == nil) != approve {
|
||||
t.Fatalf("approve=%t but the file exists=%t; results:\n%s", approve, statErr == nil, strings.Join(prov.toolResults(), "\n"))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,9 @@ type Result struct {
|
||||
Blocked bool
|
||||
// BlockReason is the extension's user-visible reason, credential-redacted.
|
||||
BlockReason string
|
||||
// Ask is set when a tighten-only runtime put the call to the user; the caller
|
||||
// asks even where the host allowed it. It is never set by an allow or a deny.
|
||||
Ask bool
|
||||
// Permission carries the terminal allow/deny ruling at permission.decision;
|
||||
// nil means every interceptor continued and the host decision stands. The
|
||||
// CALLER combines it with the host verdict (host deny + extension allow →
|
||||
@@ -280,6 +283,15 @@ func (d *Dispatcher) Intercept(ctx context.Context, point extension.InterceptorP
|
||||
return nil, fmt.Errorf("dispatch: marshal %s replacement: %w", point, err)
|
||||
}
|
||||
result.Applied = append(result.Applied, pluginID)
|
||||
case protocol.DecisionAsk:
|
||||
if point != extension.PointPermissionDecision || !d.tightenOnly[pluginID] {
|
||||
err := fmt.Errorf("decision %q is only legal for a tighten-only runtime at %s", answer.Decision, extension.PointPermissionDecision)
|
||||
if err := d.violation(pluginID, point, err); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
result.Ask = true
|
||||
case protocol.DecisionAllow, protocol.DecisionDeny:
|
||||
if point != extension.PointPermissionDecision {
|
||||
err := fmt.Errorf("decision %q is only legal at %s", answer.Decision, extension.PointPermissionDecision)
|
||||
|
||||
@@ -97,3 +97,38 @@ func TestARequiredTightenOnlyRuntimeThatLoosensFailsTheOperationWithATypedCause(
|
||||
t.Fatalf("err = %v, want a ViolationError wrapping ErrLoosensDecision", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnAskFromATightenOnlyRuntimeIsRecordedAndNotMistakenForAnAllow(t *testing.T) {
|
||||
point := extension.PointPermissionDecision
|
||||
d := tightenOnlyDispatcher("mod-x", protocol.InterceptResult{Decision: protocol.DecisionAsk}, &warnRecorder{})
|
||||
result, err := d.Intercept(context.Background(), point, &PermissionPayload{Name: "bash", HostDecision: "allow"})
|
||||
if err != nil || !result.Ask || result.Permission != nil {
|
||||
t.Fatalf("result = %+v, %v; want Ask set and no allow/deny", result, err)
|
||||
}
|
||||
}
|
||||
|
||||
// ask is the tighten-only runtimes' own ruling; another runtime, or any other
|
||||
// point, answering it breaks the dispatch rules.
|
||||
func TestAnAskFromAnyoneElseOrAnywhereElseIsAViolation(t *testing.T) {
|
||||
ask := &fakeClient{interceptFn: func(protocol.InterceptEvent, json.RawMessage) (protocol.InterceptResult, error) {
|
||||
return protocol.InterceptResult{Decision: protocol.DecisionAsk}, nil
|
||||
}}
|
||||
for name, tc := range map[string]struct {
|
||||
point extension.InterceptorPoint
|
||||
tight map[string]bool
|
||||
pay any
|
||||
}{
|
||||
"not tighten-only": {extension.PointPermissionDecision, nil, &PermissionPayload{Name: "bash", HostDecision: "allow"}},
|
||||
"other point": {extension.PointToolBefore, map[string]bool{"mod-x": true}, &ToolBeforePayload{Name: "bash", Arguments: "{}"}},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
warns := &warnRecorder{}
|
||||
d := New(map[extension.InterceptorPoint][]extension.Contribution{tc.point: {interceptor("mod-x", tc.point, 0)}}, nil,
|
||||
func(string) Client { return ask }, nil, Options{Warn: warns.warn, TightenOnly: tc.tight})
|
||||
result, err := d.Intercept(context.Background(), tc.point, tc.pay)
|
||||
if err != nil || result.Ask || warns.count() != 1 {
|
||||
t.Fatalf("result = %+v, %v, warnings %d; want the ask refused with one warning", result, err, warns.count())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -88,6 +88,9 @@ const (
|
||||
DecisionReplace InterceptDecision = "replace"
|
||||
DecisionAllow InterceptDecision = "allow"
|
||||
DecisionDeny InterceptDecision = "deny"
|
||||
// DecisionAsk, legal only at permission.decision and only for a runtime that
|
||||
// may merely tighten, puts the call to the user even where the host allowed it.
|
||||
DecisionAsk InterceptDecision = "ask"
|
||||
)
|
||||
|
||||
// UIHostKind identifies which host UI surface family renders extension UI.
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"$defs":{"CapabilityWire":{"additionalProperties":false,"properties":{"id":{"minLength":1,"type":"string"},"kind":{"minLength":1,"type":"string"},"namespace":{"minLength":1,"type":"string"},"schemaHash":{"type":"string"},"version":{"type":"string"}},"required":["id","kind","namespace"],"type":"object"},"ContentReadParams":{"additionalProperties":false,"properties":{"contentRef":{"minLength":1,"type":"string"},"offset":{"minimum":0,"type":"integer"}},"required":["contentRef","offset"],"type":"object"},"ContentReadResult":{"additionalProperties":false,"properties":{"contentRef":{"minLength":1,"type":"string"},"dataBase64":{"type":"string"},"encoding":{"enum":["utf8"],"type":"string"},"nextOffset":{"minimum":0,"type":"integer"},"offset":{"minimum":0,"type":"integer"},"sha256":{"type":"string"},"totalBytes":{"minimum":0,"type":"integer"}},"required":["contentRef","dataBase64","encoding","offset","sha256","totalBytes"],"type":"object"},"EventParams":{"additionalProperties":false,"properties":{"event":{"enum":["agent.before_start","compaction.complete","compaction.prepare","context.prepare","frontend.event","input.receive","permission.decision","provider.request","provider.response","session.end","session.load","session.rotate","session.save","session.start","system_prompt.build","tool.after","tool.before"],"type":"string"},"externalized":{"items":{"$ref":"#/$defs/ExternalizedField"},"type":"array"},"payload":{"x-externalizable":true}},"required":["event","payload"],"type":"object"},"ExternalizedField":{"additionalProperties":false,"properties":{"contentRef":{"minLength":1,"type":"string"},"jsonPointer":{"minLength":1,"type":"string"},"sha256":{"type":"string"},"totalBytes":{"minimum":0,"type":"integer"}},"required":["contentRef","jsonPointer","sha256","totalBytes"],"type":"object"},"HostCapabilities":{"additionalProperties":false,"properties":{"contentRefs":{"type":"boolean"},"dependencySchemaVersion":{"minimum":0,"type":"integer"},"protocolVersion":{"minLength":1,"type":"string"},"uiHost":{"enum":["acp","desktop","headless","tui"],"type":"string"}},"required":["contentRefs","protocolVersion","uiHost"],"type":"object"},"InitializeParams":{"additionalProperties":false,"properties":{"capabilities":{"$ref":"#/$defs/HostCapabilities"},"dependencySchemaVersion":{"minimum":0,"type":"integer"},"manifest":{"$ref":"#/$defs/ManifestExpectation"},"protocolId":{"minLength":1,"type":"string"},"protocolVersion":{"minLength":1,"type":"string"},"session":{"$ref":"#/$defs/SessionContext"}},"required":["capabilities","manifest","protocolId","protocolVersion","session"],"type":"object"},"InitializeResult":{"additionalProperties":false,"properties":{"componentId":{"type":"string"},"name":{"minLength":1,"type":"string"},"protocolVersion":{"minLength":1,"type":"string"},"providers":{"items":{"$ref":"#/$defs/ProviderDescriptor"},"type":"array"},"provides":{"items":{"$ref":"#/$defs/CapabilityWire"},"type":"array"},"replaces":{"items":{"type":"string"},"type":"array"},"requires":{"items":{"$ref":"#/$defs/RequirementWire"},"type":"array"},"stateSchemaVersion":{"minimum":0,"type":"integer"},"subscriptions":{"items":{"type":"string"},"type":"array"},"toolSpecs":{"items":{"$ref":"#/$defs/ToolSpec"},"type":"array"},"tools":{"items":{"type":"string"},"type":"array"},"uiActions":{"items":{"$ref":"#/$defs/UIActionDecl"},"type":"array"},"version":{"minLength":1,"type":"string"}},"required":["name","protocolVersion","stateSchemaVersion","version"],"type":"object"},"InitializedParams":{"additionalProperties":false,"properties":{},"type":"object"},"InterceptParams":{"additionalProperties":false,"properties":{"event":{"enum":["agent.before_start","compaction.complete","compaction.prepare","context.prepare","frontend.event","input.receive","permission.decision","provider.request","provider.response","session.end","session.load","session.rotate","session.save","session.start","system_prompt.build","tool.after","tool.before"],"type":"string"},"externalized":{"items":{"$ref":"#/$defs/ExternalizedField"},"type":"array"},"payload":{"x-externalizable":true},"seq":{"minimum":1,"type":"integer"},"timeoutMillis":{"minimum":0,"type":"integer"}},"required":["event","payload","seq","timeoutMillis"],"type":"object"},"InterceptResult":{"additionalProperties":false,"properties":{"decision":{"enum":["allow","block","continue","deny","replace"],"type":"string"},"externalized":{"items":{"$ref":"#/$defs/ExternalizedField"},"type":"array"},"reason":{"type":"string"},"replacement":{"x-externalizable":true}},"required":["decision"],"type":"object"},"ManifestExpectation":{"additionalProperties":false,"properties":{"capabilities":{"items":{"type":"string"},"type":"array"},"intercepts":{"items":{"type":"string"},"type":"array"},"providers":{"items":{"type":"string"},"type":"array"},"provides":{"items":{"$ref":"#/$defs/CapabilityWire"},"type":"array"},"replaces":{"items":{"type":"string"},"type":"array"},"requires":{"items":{"$ref":"#/$defs/RequirementWire"},"type":"array"}Line truncated
|
||||
{"$defs":{"CapabilityWire":{"additionalProperties":false,"properties":{"id":{"minLength":1,"type":"string"},"kind":{"minLength":1,"type":"string"},"namespace":{"minLength":1,"type":"string"},"schemaHash":{"type":"string"},"version":{"type":"string"}},"required":["id","kind","namespace"],"type":"object"},"ContentReadParams":{"additionalProperties":false,"properties":{"contentRef":{"minLength":1,"type":"string"},"offset":{"minimum":0,"type":"integer"}},"required":["contentRef","offset"],"type":"object"},"ContentReadResult":{"additionalProperties":false,"properties":{"contentRef":{"minLength":1,"type":"string"},"dataBase64":{"type":"string"},"encoding":{"enum":["utf8"],"type":"string"},"nextOffset":{"minimum":0,"type":"integer"},"offset":{"minimum":0,"type":"integer"},"sha256":{"type":"string"},"totalBytes":{"minimum":0,"type":"integer"}},"required":["contentRef","dataBase64","encoding","offset","sha256","totalBytes"],"type":"object"},"EventParams":{"additionalProperties":false,"properties":{"event":{"enum":["agent.before_start","compaction.complete","compaction.prepare","context.prepare","frontend.event","input.receive","permission.decision","provider.request","provider.response","session.end","session.load","session.rotate","session.save","session.start","system_prompt.build","tool.after","tool.before"],"type":"string"},"externalized":{"items":{"$ref":"#/$defs/ExternalizedField"},"type":"array"},"payload":{"x-externalizable":true}},"required":["event","payload"],"type":"object"},"ExternalizedField":{"additionalProperties":false,"properties":{"contentRef":{"minLength":1,"type":"string"},"jsonPointer":{"minLength":1,"type":"string"},"sha256":{"type":"string"},"totalBytes":{"minimum":0,"type":"integer"}},"required":["contentRef","jsonPointer","sha256","totalBytes"],"type":"object"},"HostCapabilities":{"additionalProperties":false,"properties":{"contentRefs":{"type":"boolean"},"dependencySchemaVersion":{"minimum":0,"type":"integer"},"protocolVersion":{"minLength":1,"type":"string"},"uiHost":{"enum":["acp","desktop","headless","tui"],"type":"string"}},"required":["contentRefs","protocolVersion","uiHost"],"type":"object"},"InitializeParams":{"additionalProperties":false,"properties":{"capabilities":{"$ref":"#/$defs/HostCapabilities"},"dependencySchemaVersion":{"minimum":0,"type":"integer"},"manifest":{"$ref":"#/$defs/ManifestExpectation"},"protocolId":{"minLength":1,"type":"string"},"protocolVersion":{"minLength":1,"type":"string"},"session":{"$ref":"#/$defs/SessionContext"}},"required":["capabilities","manifest","protocolId","protocolVersion","session"],"type":"object"},"InitializeResult":{"additionalProperties":false,"properties":{"componentId":{"type":"string"},"name":{"minLength":1,"type":"string"},"protocolVersion":{"minLength":1,"type":"string"},"providers":{"items":{"$ref":"#/$defs/ProviderDescriptor"},"type":"array"},"provides":{"items":{"$ref":"#/$defs/CapabilityWire"},"type":"array"},"replaces":{"items":{"type":"string"},"type":"array"},"requires":{"items":{"$ref":"#/$defs/RequirementWire"},"type":"array"},"stateSchemaVersion":{"minimum":0,"type":"integer"},"subscriptions":{"items":{"type":"string"},"type":"array"},"toolSpecs":{"items":{"$ref":"#/$defs/ToolSpec"},"type":"array"},"tools":{"items":{"type":"string"},"type":"array"},"uiActions":{"items":{"$ref":"#/$defs/UIActionDecl"},"type":"array"},"version":{"minLength":1,"type":"string"}},"required":["name","protocolVersion","stateSchemaVersion","version"],"type":"object"},"InitializedParams":{"additionalProperties":false,"properties":{},"type":"object"},"InterceptParams":{"additionalProperties":false,"properties":{"event":{"enum":["agent.before_start","compaction.complete","compaction.prepare","context.prepare","frontend.event","input.receive","permission.decision","provider.request","provider.response","session.end","session.load","session.rotate","session.save","session.start","system_prompt.build","tool.after","tool.before"],"type":"string"},"externalized":{"items":{"$ref":"#/$defs/ExternalizedField"},"type":"array"},"payload":{"x-externalizable":true},"seq":{"minimum":1,"type":"integer"},"timeoutMillis":{"minimum":0,"type":"integer"}},"required":["event","payload","seq","timeoutMillis"],"type":"object"},"InterceptResult":{"additionalProperties":false,"properties":{"decision":{"enum":["allow","ask","block","continue","deny","replace"],"type":"string"},"externalized":{"items":{"$ref":"#/$defs/ExternalizedField"},"type":"array"},"reason":{"type":"string"},"replacement":{"x-externalizable":true}},"required":["decision"],"type":"object"},"ManifestExpectation":{"additionalProperties":false,"properties":{"capabilities":{"items":{"type":"string"},"type":"array"},"intercepts":{"items":{"type":"string"},"type":"array"},"providers":{"items":{"type":"string"},"type":"array"},"provides":{"items":{"$ref":"#/$defs/CapabilityWire"},"type":"array"},"replaces":{"items":{"type":"string"},"type":"array"},"requires":{"items":{"$ref":"#/$defs/RequirementWire"},"type":"aLine truncated
|
||||
@@ -5,4 +5,4 @@ package protocol
|
||||
// GeneratedSchemaHash is the SHA-256 of schema.generated.json. Handshake
|
||||
// comparisons use this constant; protocol tests independently recompute it
|
||||
// from CanonicalSchemaBytes to reject stale generated artifacts.
|
||||
const GeneratedSchemaHash = "sha256:3c01970cdea893303c49150fb2717f592178fb3974a17c91a7848712e516930d"
|
||||
const GeneratedSchemaHash = "sha256:d46a970c1b1ac3a03cbdc9a32e6457e48f02c25a2679a0e3bbc8a727557437e7"
|
||||
@@ -32,7 +32,7 @@ var enumTypes = map[reflect.Type][]string{
|
||||
reflect.TypeFor[Direction](): values(DirectionHostToExtensionRequest, DirectionExtensionToHostRequest, DirectionHostToExtensionNotification, DirectionExtensionToHostNotification),
|
||||
reflect.TypeFor[OperationClass](): values(ClassLifecycle, ClassIntercept, ClassObservation, ClassProvider, ClassUI, ClassContent),
|
||||
reflect.TypeFor[InterceptEvent](): interceptEventValues(),
|
||||
reflect.TypeFor[InterceptDecision](): values(DecisionContinue, DecisionBlock, DecisionReplace, DecisionAllow, DecisionDeny),
|
||||
reflect.TypeFor[InterceptDecision](): values(DecisionContinue, DecisionBlock, DecisionReplace, DecisionAllow, DecisionDeny, DecisionAsk),
|
||||
reflect.TypeFor[UIHostKind](): values(UIHostTUI, UIHostDesktop, UIHostACP, UIHostHeadless),
|
||||
reflect.TypeFor[UISurfaceKind](): values(UISurfaceStatus, UISurfaceCard, UISurfaceForm, UISurfaceNotification, UISurfacePanel, UISurfaceView),
|
||||
reflect.TypeFor[UINodeKind](): values(UINodeText, UINodeMarkdown, UINodeRow, UINodeStack, UINodeKV, UINodeMeter, UINodePip, UINodeButton, UINodeDivider),
|
||||
|
||||
@@ -2,6 +2,7 @@ package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
"reasonix/internal/ext/extension"
|
||||
"reasonix/internal/ext/extension/dispatch"
|
||||
"reasonix/internal/ext/extension/providerconv"
|
||||
"reasonix/internal/safety/permission"
|
||||
)
|
||||
|
||||
// Extension Protocol v2 agent-side wiring. The agent consults the
|
||||
@@ -349,6 +351,32 @@ func (a *Agent) interceptExtensionPermission(ctx context.Context, plan *toolCall
|
||||
case result.Permission != nil && *result.Permission:
|
||||
*allow = true
|
||||
}
|
||||
if result.Ask && *allow {
|
||||
return a.confirmAskedByExtension(ctx, plan)
|
||||
}
|
||||
return toolOutcome{}, false
|
||||
}
|
||||
|
||||
// confirmAskedByExtension puts a call the host allowed to the user because a
|
||||
// tighten-only extension asked. A gate that cannot ask refuses: an ask is never
|
||||
// quietly dropped.
|
||||
func (a *Agent) confirmAskedByExtension(ctx context.Context, plan *toolCallPlan) (toolOutcome, bool) {
|
||||
refuse := func(msg string) (toolOutcome, bool) {
|
||||
return toolOutcome{output: "blocked: " + msg, blocked: true, errMsg: "blocked by extension permission policy"}, true
|
||||
}
|
||||
c, ok := a.svc.gate.(interface {
|
||||
Confirm(context.Context, string, json.RawMessage, string) (permission.Verdict, error)
|
||||
})
|
||||
if !ok {
|
||||
return refuse("an extension required approval for this call and this session has no way to ask")
|
||||
}
|
||||
v, err := c.Confirm(ctx, plan.permName, plan.permArgs, "an extension")
|
||||
if err != nil {
|
||||
return refuse(err.Error())
|
||||
}
|
||||
if !v.Allow {
|
||||
return refuse(v.Reason)
|
||||
}
|
||||
return toolOutcome{}, false
|
||||
}
|
||||
|
||||
|
||||
@@ -159,6 +159,34 @@ func (g *Gate) denial(ruleReason string) Verdict {
|
||||
}
|
||||
}
|
||||
|
||||
// ConfirmTool names the approval a stricter party opens on a call the policy
|
||||
// allowed. It is a fresh decision: a person answers it, never a posture such as
|
||||
// YOLO or auto, and the answer covers this one call.
|
||||
const ConfirmTool = "mod_check"
|
||||
|
||||
// Confirm puts a call the policy allowed to the user anyway, on behalf of who
|
||||
// (a Claude Code mod that may only tighten). It cannot loosen anything: the
|
||||
// verdict is Allow only when a person approved, and a gate with nobody to ask
|
||||
// refuses.
|
||||
func (g *Gate) Confirm(ctx context.Context, toolName string, args json.RawMessage, who string) (Verdict, error) {
|
||||
if g.Approver == nil {
|
||||
return Verdict{Reason: who + " asked for approval of this call and nobody can give it, so it was refused.", Code: RefusalUnattended}, nil
|
||||
}
|
||||
subject := who + " asks you to approve: " + toolName + " " + Subject(args)
|
||||
allow, _, reason, err := g.approve(ctx, ConfirmTool, subject, args, who+" requires approval for this call")
|
||||
if err != nil {
|
||||
return Verdict{Reason: "approval aborted"}, err
|
||||
}
|
||||
if !allow {
|
||||
v := Verdict{Reason: "the user declined this tool call — do not retry it; ask how they would like to proceed or choose another approach.", Code: RefusalDeclined}
|
||||
if reason != "" {
|
||||
v.Reason = reason
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
return Verdict{Allow: true}, nil
|
||||
}
|
||||
|
||||
// Checker is any gate a tool call is put to.
|
||||
type Checker interface {
|
||||
Check(ctx context.Context, toolName string, args json.RawMessage, readOnly bool) (bool, string, error)
|
||||
|
||||
@@ -633,7 +633,7 @@ func ParseToolApprovalMode(mode string) (string, bool) {
|
||||
// narrow operation in interactive or headless mode.
|
||||
func RequiresFreshHumanApprovalTool(tool string) bool {
|
||||
switch tool {
|
||||
case planApprovalTool, memoryRememberTool, memoryForgetTool, SandboxEscapeApprovalTool, ManagedConfigWriteApprovalTool, NetworkEgressApprovalTool, PaidSearchApprovalTool, MCPSignInApprovalTool, MCPScopeUpgradeApprovalTool, SettingApprovalTool, SettingElevatedApprovalTool:
|
||||
case planApprovalTool, memoryRememberTool, memoryForgetTool, permission.ConfirmTool, SandboxEscapeApprovalTool, ManagedConfigWriteApprovalTool, NetworkEgressApprovalTool, PaidSearchApprovalTool, MCPSignInApprovalTool, MCPScopeUpgradeApprovalTool, SettingApprovalTool, SettingElevatedApprovalTool:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
||||
@@ -276,6 +276,7 @@ const (
|
||||
DecisionReplace InterceptDecision = "replace"
|
||||
DecisionAllow InterceptDecision = "allow"
|
||||
DecisionDeny InterceptDecision = "deny"
|
||||
DecisionAsk InterceptDecision = "ask"
|
||||
)
|
||||
|
||||
// ProviderCatalogParams is a generated Extension Protocol v2 wire DTO.
|
||||
|
||||
Reference in new issue
Block a user