diff --git a/docs/EXTENSION_PROTOCOL.generated.md b/docs/EXTENSION_PROTOCOL.generated.md index 9aeaaeaa27..19aa19d764 100644 --- a/docs/EXTENSION_PROTOCOL.generated.md +++ b/docs/EXTENSION_PROTOCOL.generated.md @@ -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 diff --git a/internal/assembly/boot/effect_mod_host_test.go b/internal/assembly/boot/effect_mod_host_test.go index 37c7181dce..f1f3f1fab4 100644 --- a/internal/assembly/boot/effect_mod_host_test.go +++ b/internal/assembly/boot/effect_mod_host_test.go @@ -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")) + } + }) + } +} diff --git a/internal/ext/extension/dispatch/dispatch.go b/internal/ext/extension/dispatch/dispatch.go index a3f05f27ec..20a588cff2 100644 --- a/internal/ext/extension/dispatch/dispatch.go +++ b/internal/ext/extension/dispatch/dispatch.go @@ -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) diff --git a/internal/ext/extension/dispatch/tighten_only_test.go b/internal/ext/extension/dispatch/tighten_only_test.go index a129e941ca..1740c319b8 100644 --- a/internal/ext/extension/dispatch/tighten_only_test.go +++ b/internal/ext/extension/dispatch/tighten_only_test.go @@ -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()) + } + }) + } +} diff --git a/internal/ext/extension/protocol/enums.go b/internal/ext/extension/protocol/enums.go index 21f90771b7..ed03e65bef 100644 --- a/internal/ext/extension/protocol/enums.go +++ b/internal/ext/extension/protocol/enums.go @@ -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. diff --git a/internal/ext/extension/protocol/schema.generated.json b/internal/ext/extension/protocol/schema.generated.json index e7fbb0c855..1d40009131 100644 --- a/internal/ext/extension/protocol/schema.generated.json +++ b/internal/ext/extension/protocol/schema.generated.json @@ -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"},"tools":{"items":{"type":"string"},"type":"array"},"uiActions":{"items":{"type":"string"},"type":"array"}},"type":"object"},"ProviderCatalogParams":{"additionalProperties":false,"properties":{},"type":"object"},"ProviderCatalogResult":{"additionalProperties":false,"properties":{"providers":{"items":{"$ref":"#/$defs/ProviderDescriptor"},"type":"array"}},"required":["providers"],"type":"object"},"ProviderChunk":{"additionalProperties":false,"properties":{"argChars":{"minimum":0,"type":"integer"},"epoch":{"type":"string"},"error":{"$ref":"#/$defs/ProviderError"},"generation":{"type":"integer"},"signature":{"type":"string"},"text":{"type":"string"},"toolCall":{"$ref":"#/$defs/ProviderToolCall"},"type":{"enum":["done","error","reasoning","text","tool_call","tool_call_args_delta","tool_call_start","usage"],"type":"string"},"usage":{"$ref":"#/$defs/ProviderUsage"}},"required":["type"],"type":"object"},"ProviderDescriptor":{"additionalProperties":false,"properties":{"cacheHitPerMillion":{"minimum":0,"type":"number"},"contextWindow":{"minimum":0,"type":"integer"},"defaultEffort":{"type":"string"},"displayName":{"type":"string"},"efforts":{"items":{"type":"string"},"type":"array"},"inputPerMillion":{"minimum":0,"type":"number"},"model":{"type":"string"},"outputPerMillion":{"minimum":0,"type":"number"},"pricingCurrency":{"type":"string"},"reasoning":{"type":"boolean"},"reasoningRoundTrip":{"type":"boolean"},"ref":{"minLength":1,"type":"string"},"toolCallReasoning":{"type":"boolean"},"tools":{"type":"boolean"},"vision":{"type":"boolean"},"warnOnMissingToolCallReasoning":{"type":"boolean"}},"required":["ref"],"type":"object"},"ProviderError":{"additionalProperties":false,"properties":{"code":{"enum":["provider_failed","provider_interrupted"],"type":"string"},"message":{"minLength":1,"type":"string"}},"required":["code","message"],"type":"object"},"ProviderMessage":{"additionalProperties":false,"properties":{"content":{"type":"string","x-externalizable":true},"images":{"items":{"type":"string"},"type":"array"},"name":{"type":"string"},"reasoning_content":{"type":"string"},"reasoning_signature":{"type":"string"},"role":{"enum":["assistant","system","tool","user"],"type":"string"},"tool_call_id":{"type":"string"},"tool_calls":{"items":{"$ref":"#/$defs/ProviderToolCall"},"type":"array"}},"type":"object"},"ProviderRequest":{"additionalProperties":false,"properties":{"maxTokens":{"minimum":0,"type":"integer"},"messages":{"items":{"$ref":"#/$defs/ProviderMessage"},"type":"array"},"responseFormat":{"$ref":"#/$defs/ProviderResponseFormat"},"temperature":{"type":"number"},"tools":{"items":{"$ref":"#/$defs/ProviderToolSchema"},"type":"array"}},"required":["maxTokens","messages","tools"],"type":"object"},"ProviderResponseFormat":{"additionalProperties":false,"properties":{"type":{"minLength":1,"type":"string"}},"required":["type"],"type":"object"},"ProviderToolCall":{"additionalProperties":false,"properties":{"arguments":{"type":"string"},"id":{"minLength":1,"type":"string"},"name":{"minLength":1,"type":"string"},"thought_signature":{"type":"string"}},"required":["arguments","id","name"],"type":"object"},"ProviderToolSchema":{"additionalProperties":false,"properties":{"description":{"type":"string"},"name":{"minLength":1,"type":"string"},"parameters":true},"required":["name","parameters"],"type":"object"},"ProviderUsage":{"additionalProperties":false,"properties":{"cacheHitTokens":{"minimum":0,"type":"integer"},"cacheMissTokens":{"minimum":0,"type":"integer"},"completionTokens":{"minimum":0,"type":"integer"},"finishReason":{"type":"string"},"promptTokens":{"minimum":0,"type":"integer"},"reasoningTokens":{"minimum":0,"type":"integer"},"totalTokens":{"minimum":0,"type":"integer"}},"required":["cacheHitTokens","cacheMissTokens","completionTokens","promptTokens","reasoningTokens","totalTokens"],"type":"object"},"RequirementWire":{"additionalProperties":false,"properties":{"id":{"minLength":1,"type":"string"},"kind":{"minLength":1,"type":"string"},"namespace":{"minLength":1,"type":"string"},"optional":{"type":"boolean"},"schemaHash":{"type":"string"},"version":{"type":"string"},"versionRange":{"type":"string"}},"required":["id","kind","namespace"],"type":"object"},"ResourcesChangedParams":{"additionalProperties":false,"properties":{"paths":{"items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"},"SessionContext":{"additionalProperties":false,"properties":{"epoch":{"type":"string"},"generation":{"type":"integer"},"sessionId":{"minLength":1,"type":"string"},"workspaceRoot":{"minLength":1,"type":"string"}},"required":["generation","sessionId","workspaceRoot"],"type":"object"},"ShutdownParams":{"additionalProperties":false,"properties":{"timeoutMillis":{"minimum":0,"type":"integer"}},"required":["timeoutMillis"],"type":"object"},"ShutdownResult":{"additionalProperties":false,"properties":{"accepted":{"type":"boolean"}},"required":["accepted"],"type":"object"},"StreamCancelParams":{"additionalProperties":false,"properties":{"streamId":{"minLength":1,"type":"string"}},"required":["streamId"],"type":"object"},"StreamCancelResult":{"additionalProperties":false,"properties":{"cancelled":{"type":"boolean"}},"required":["cancelled"],"type":"object"},"StreamChunkParams":{"additionalProperties":false,"properties":{"chunk":{"$ref":"#/$defs/ProviderChunk"},"epoch":{"type":"string"},"generation":{"type":"integer"},"seq":{"minimum":1,"type":"integer"},"streamId":{"minLength":1,"type":"string"}},"required":["chunk","seq","streamId"],"type":"object"},"StreamEndParams":{"additionalProperties":false,"properties":{"error":{"type":"string"},"interrupted":{"type":"boolean"},"lastSeq":{"minimum":0,"type":"integer"},"streamId":{"minLength":1,"type":"string"}},"required":["lastSeq","streamId"],"type":"object"},"StreamOpenParams":{"additionalProperties":false,"properties":{"effort":{"type":"string"},"epoch":{"type":"string"},"generation":{"type":"integer"},"model":{"type":"string"},"providerRef":{"minLength":1,"type":"string"},"request":{"$ref":"#/$defs/ProviderRequest"},"seqBase":{"minimum":0,"type":"integer"},"streamId":{"minLength":1,"type":"string"}},"required":["providerRef","request","seqBase","streamId"],"type":"object"},"StreamOpenResult":{"additionalProperties":false,"properties":{"accepted":{"type":"boolean"}},"required":["accepted"],"type":"object"},"ToolCallParams":{"additionalProperties":false,"properties":{"arguments":true,"name":{"minLength":1,"type":"string"},"timeoutMillis":{"minimum":0,"type":"integer"}},"required":["arguments","name","timeoutMillis"],"type":"object"},"ToolCallResult":{"additionalProperties":false,"properties":{"content":{"type":"string"},"isError":{"type":"boolean"}},"required":["content"],"type":"object"},"ToolSpec":{"additionalProperties":false,"properties":{"description":{"minLength":1,"type":"string"},"inputSchema":true,"name":{"minLength":1,"type":"string"},"readOnly":{"type":"boolean"}},"required":["description","inputSchema","name"],"type":"object"},"UIActionDecl":{"additionalProperties":false,"properties":{"actionId":{"minLength":1,"type":"string"},"label":{"type":"string"}},"required":["actionId"],"type":"object"},"UIActionParams":{"additionalProperties":false,"properties":{"actionId":{"minLength":1,"type":"string"},"args":{"additionalProperties":{"type":"string"},"type":"object"},"generation":{"type":"integer"},"sessionId":{"minLength":1,"type":"string"}},"required":["actionId","generation","sessionId"],"type":"object"},"UIActionResult":{"additionalProperties":false,"properties":{"accepted":{"type":"boolean"},"message":{"type":"string"}},"required":["accepted"],"type":"object"},"UIPublishParams":{"additionalProperties":false,"properties":{"generation":{"type":"integer"},"kind":{"enum":["card","form","notification","panel","status","view"],"type":"string"},"payload":true,"sessionId":{"minLength":1,"type":"string"},"surfaceId":{"minLength":1,"type":"string"}},"required":["generation","kind","payload","sessionId","surfaceId"],"type":"object"},"UIPublishResult":{"additionalProperties":false,"properties":{"accepted":{"type":"boolean"}},"required":["accepted"],"type":"object"},"UIRequestParams":{"additionalProperties":false,"properties":{"generation":{"type":"integer"},"kind":{"enum":["confirm","input","multiselect","select"],"type":"string"},"payload":true,"sessionId":{"minLength":1,"type":"string"},"surfaceId":{"minLength":1,"type":"string"}},"required":["generation","kind","payload","sessionId","surfaceId"],"type":"object"},"UIRequestResult":{"additionalProperties":false,"properties":{"cancelled":{"type":"boolean"},"values":{"additionalProperties":true,"type":"object"}},"required":["cancelled"],"type":"object"},"UISubmitParams":{"additionalProperties":false,"properties":{"generation":{"type":"integer"},"sessionId":{"minLength":1,"type":"string"},"surfaceId":{"minLength":1,"type":"string"},"values":{"additionalProperties":true,"type":"object"}},"required":["generation","sessionId","surfaceId","values"],"type":"object"},"UISubmitResult":{"additionalProperties":false,"properties":{"accepted":{"type":"boolean"}},"required":["accepted"],"type":"object"}},"$id":"reasonix.extension.v2","$schema":"https://json-schema.org/draft/2020-12/schema","errors":[{"jsonRpcCode":-32000,"message":"Component activation failed; the new generation was not published.","reason":"activation_failed","retryable":false},{"jsonRpcCode":-32000,"message":"The extension used a capability its manifest did not declare.","reason":"capability_not_declared","retryable":false},{"jsonRpcCode":-32000,"message":"Component cleanup failed while disposing scoped effects.","reason":"cleanup_failed","retryable":true},{"jsonRpcCode":-32000,"message":"The referenced content has expired.","reason":"content_ref_expired","retryable":true},{"jsonRpcCode":-32000,"message":"The dependency graph contains a required cycle.","reason":"dependency_cycle","retryable":false},{"jsonRpcCode":-32000,"message":"A required dependency is missing or not version-compatible.","reason":"dependency_unsatisfied","retryable":false},{"jsonRpcCode":-32000,"message":"The frame exceeds the frozen protocol size limit.","reason":"frame_too_large","retryable":false},{"jsonRpcCode":-32000,"message":"The extension did not answer an intercept within its timeout.","reason":"intercept_timeout","retryable":true},{"jsonRpcCode":-32603,"message":"An internal extension protocol error occurred.","reason":"internal","retryable":true},{"jsonRpcCode":-32602,"message":"The params do not match the registered method schema.","reason":"invalid_params","retryable":false},{"jsonRpcCode":-32600,"message":"The extension protocol frame or envelope is invalid.","reason":"protocol_error","retryable":false},{"jsonRpcCode":-32000,"message":"The extension provider stream failed.","reason":"provider_failed","retryable":true},{"jsonRpcCode":-32000,"message":"The extension provider stream was interrupted.","reason":"provider_interrupted","retryable":true},{"jsonRpcCode":-32000,"message":"A capability schema hash does not match the expected pin.","reason":"schema_mismatch","retryable":false},{"jsonRpcCode":-32000,"message":"The extension did not shut down within the requested timeout.","reason":"shutdown_timeout","retryable":true},{"jsonRpcCode":-32000,"message":"The message belongs to a superseded runtime generation.","reason":"stale_generation","retryable":false},{"jsonRpcCode":-32000,"message":"The provider stream was cancelled.","reason":"stream_cancelled","retryable":false},{"jsonRpcCode":-32000,"message":"A provider stream chunk is missing from the ordered sequence.","reason":"stream_gap","retryable":true},{"jsonRpcCode":-32601,"message":"The method is not registered in Extension Protocol v2.","reason":"unknown_method","retryable":false},{"jsonRpcCode":-32000,"message":"The peer's extension protocol version is not supported.","reason":"unsupported_version","retryable":false}],"interceptEvents":["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"],"limits":{"contentRefChunkBytes":262144,"contentRefObjectBytes":8388608,"externalizeFieldBytes":65536,"frameBytes":8388608},"methods":{"extension/event":{"class":"observation","direction":"host_to_extension_notification","notification":true,"params":{"$ref":"#/$defs/EventParams"},"result":null},"extension/initialize":{"class":"lifecycle","direction":"host_to_extension_request","notification":false,"params":{"$ref":"#/$defs/InitializeParams"},"result":{"$ref":"#/$defs/InitializeResult"}},"extension/initialized":{"class":"lifecycle","direction":"host_to_extension_notification","notification":true,"params":{"$ref":"#/$defs/InitializedParams"},"result":null},"extension/intercept":{"class":"intercept","direction":"host_to_extension_request","notification":false,"params":{"$ref":"#/$defs/InterceptParams"},"result":{"$ref":"#/$defs/InterceptResult"}},"extension/provider/catalog":{"class":"provider","direction":"host_to_extension_request","notification":false,"params":{"$ref":"#/$defs/ProviderCatalogParams"},"result":{"$ref":"#/$defs/ProviderCatalogResult"}},"extension/provider/stream/cancel":{"class":"provider","direction":"host_to_extension_request","notification":false,"params":{"$ref":"#/$defs/StreamCancelParams"},"result":{"$ref":"#/$defs/StreamCancelResult"}},"extension/provider/stream/chunk":{"class":"provider","direction":"extension_to_host_notification","notification":true,"params":{"$ref":"#/$defs/StreamChunkParams"},"result":null},"extension/provider/stream/end":{"class":"provider","direction":"extension_to_host_notification","notification":true,"params":{"$ref":"#/$defs/StreamEndParams"},"result":null},"extension/provider/stream/open":{"class":"provider","direction":"host_to_extension_request","notification":false,"params":{"$ref":"#/$defs/StreamOpenParams"},"result":{"$ref":"#/$defs/StreamOpenResult"}},"extension/resources/changed":{"class":"observation","direction":"host_to_extension_notification","notification":true,"params":{"$ref":"#/$defs/ResourcesChangedParams"},"result":null},"extension/shutdown":{"class":"lifecycle","direction":"host_to_extension_request","notification":false,"params":{"$ref":"#/$defs/ShutdownParams"},"result":{"$ref":"#/$defs/ShutdownResult"}},"extension/tool/call":{"class":"tool","direction":"host_to_extension_request","notification":false,"params":{"$ref":"#/$defs/ToolCallParams"},"result":{"$ref":"#/$defs/ToolCallResult"}},"extension/ui/action":{"class":"ui","direction":"host_to_extension_request","notification":false,"params":{"$ref":"#/$defs/UIActionParams"},"result":{"$ref":"#/$defs/UIActionResult"}},"extension/ui/submit":{"class":"ui","direction":"host_to_extension_request","notification":false,"params":{"$ref":"#/$defs/UISubmitParams"},"result":{"$ref":"#/$defs/UISubmitResult"}},"host/content/read":{"class":"content","direction":"extension_to_host_request","notification":false,"params":{"$ref":"#/$defs/ContentReadParams"},"result":{"$ref":"#/$defs/ContentReadResult"}},"host/ui/publish":{"class":"ui","direction":"extension_to_host_request","notification":false,"params":{"$ref":"#/$defs/UIPublishParams"},"result":{"$ref":"#/$defs/UIPublishResult"}},"host/ui/request":{"class":"ui","direction":"extension_to_host_request","notification":false,"params":{"$ref":"#/$defs/UIRequestParams"},"result":{"$ref":"#/$defs/UIRequestResult"}}},"protocol":"reasonix.extension.v2","protocolID":"reasonix.extension.v2","protocolMajor":2,"title":"Reasonix Extension Protocol v2"} \ No newline at end of file +{"$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":"array"},"tools":{"items":{"type":"string"},"type":"array"},"uiActions":{"items":{"type":"string"},"type":"array"}},"type":"object"},"ProviderCatalogParams":{"additionalProperties":false,"properties":{},"type":"object"},"ProviderCatalogResult":{"additionalProperties":false,"properties":{"providers":{"items":{"$ref":"#/$defs/ProviderDescriptor"},"type":"array"}},"required":["providers"],"type":"object"},"ProviderChunk":{"additionalProperties":false,"properties":{"argChars":{"minimum":0,"type":"integer"},"epoch":{"type":"string"},"error":{"$ref":"#/$defs/ProviderError"},"generation":{"type":"integer"},"signature":{"type":"string"},"text":{"type":"string"},"toolCall":{"$ref":"#/$defs/ProviderToolCall"},"type":{"enum":["done","error","reasoning","text","tool_call","tool_call_args_delta","tool_call_start","usage"],"type":"string"},"usage":{"$ref":"#/$defs/ProviderUsage"}},"required":["type"],"type":"object"},"ProviderDescriptor":{"additionalProperties":false,"properties":{"cacheHitPerMillion":{"minimum":0,"type":"number"},"contextWindow":{"minimum":0,"type":"integer"},"defaultEffort":{"type":"string"},"displayName":{"type":"string"},"efforts":{"items":{"type":"string"},"type":"array"},"inputPerMillion":{"minimum":0,"type":"number"},"model":{"type":"string"},"outputPerMillion":{"minimum":0,"type":"number"},"pricingCurrency":{"type":"string"},"reasoning":{"type":"boolean"},"reasoningRoundTrip":{"type":"boolean"},"ref":{"minLength":1,"type":"string"},"toolCallReasoning":{"type":"boolean"},"tools":{"type":"boolean"},"vision":{"type":"boolean"},"warnOnMissingToolCallReasoning":{"type":"boolean"}},"required":["ref"],"type":"object"},"ProviderError":{"additionalProperties":false,"properties":{"code":{"enum":["provider_failed","provider_interrupted"],"type":"string"},"message":{"minLength":1,"type":"string"}},"required":["code","message"],"type":"object"},"ProviderMessage":{"additionalProperties":false,"properties":{"content":{"type":"string","x-externalizable":true},"images":{"items":{"type":"string"},"type":"array"},"name":{"type":"string"},"reasoning_content":{"type":"string"},"reasoning_signature":{"type":"string"},"role":{"enum":["assistant","system","tool","user"],"type":"string"},"tool_call_id":{"type":"string"},"tool_calls":{"items":{"$ref":"#/$defs/ProviderToolCall"},"type":"array"}},"type":"object"},"ProviderRequest":{"additionalProperties":false,"properties":{"maxTokens":{"minimum":0,"type":"integer"},"messages":{"items":{"$ref":"#/$defs/ProviderMessage"},"type":"array"},"responseFormat":{"$ref":"#/$defs/ProviderResponseFormat"},"temperature":{"type":"number"},"tools":{"items":{"$ref":"#/$defs/ProviderToolSchema"},"type":"array"}},"required":["maxTokens","messages","tools"],"type":"object"},"ProviderResponseFormat":{"additionalProperties":false,"properties":{"type":{"minLength":1,"type":"string"}},"required":["type"],"type":"object"},"ProviderToolCall":{"additionalProperties":false,"properties":{"arguments":{"type":"string"},"id":{"minLength":1,"type":"string"},"name":{"minLength":1,"type":"string"},"thought_signature":{"type":"string"}},"required":["arguments","id","name"],"type":"object"},"ProviderToolSchema":{"additionalProperties":false,"properties":{"description":{"type":"string"},"name":{"minLength":1,"type":"string"},"parameters":true},"required":["name","parameters"],"type":"object"},"ProviderUsage":{"additionalProperties":false,"properties":{"cacheHitTokens":{"minimum":0,"type":"integer"},"cacheMissTokens":{"minimum":0,"type":"integer"},"completionTokens":{"minimum":0,"type":"integer"},"finishReason":{"type":"string"},"promptTokens":{"minimum":0,"type":"integer"},"reasoningTokens":{"minimum":0,"type":"integer"},"totalTokens":{"minimum":0,"type":"integer"}},"required":["cacheHitTokens","cacheMissTokens","completionTokens","promptTokens","reasoningTokens","totalTokens"],"type":"object"},"RequirementWire":{"additionalProperties":false,"properties":{"id":{"minLength":1,"type":"string"},"kind":{"minLength":1,"type":"string"},"namespace":{"minLength":1,"type":"string"},"optional":{"type":"boolean"},"schemaHash":{"type":"string"},"version":{"type":"string"},"versionRange":{"type":"string"}},"required":["id","kind","namespace"],"type":"object"},"ResourcesChangedParams":{"additionalProperties":false,"properties":{"paths":{"items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"},"SessionContext":{"additionalProperties":false,"properties":{"epoch":{"type":"string"},"generation":{"type":"integer"},"sessionId":{"minLength":1,"type":"string"},"workspaceRoot":{"minLength":1,"type":"string"}},"required":["generation","sessionId","workspaceRoot"],"type":"object"},"ShutdownParams":{"additionalProperties":false,"properties":{"timeoutMillis":{"minimum":0,"type":"integer"}},"required":["timeoutMillis"],"type":"object"},"ShutdownResult":{"additionalProperties":false,"properties":{"accepted":{"type":"boolean"}},"required":["accepted"],"type":"object"},"StreamCancelParams":{"additionalProperties":false,"properties":{"streamId":{"minLength":1,"type":"string"}},"required":["streamId"],"type":"object"},"StreamCancelResult":{"additionalProperties":false,"properties":{"cancelled":{"type":"boolean"}},"required":["cancelled"],"type":"object"},"StreamChunkParams":{"additionalProperties":false,"properties":{"chunk":{"$ref":"#/$defs/ProviderChunk"},"epoch":{"type":"string"},"generation":{"type":"integer"},"seq":{"minimum":1,"type":"integer"},"streamId":{"minLength":1,"type":"string"}},"required":["chunk","seq","streamId"],"type":"object"},"StreamEndParams":{"additionalProperties":false,"properties":{"error":{"type":"string"},"interrupted":{"type":"boolean"},"lastSeq":{"minimum":0,"type":"integer"},"streamId":{"minLength":1,"type":"string"}},"required":["lastSeq","streamId"],"type":"object"},"StreamOpenParams":{"additionalProperties":false,"properties":{"effort":{"type":"string"},"epoch":{"type":"string"},"generation":{"type":"integer"},"model":{"type":"string"},"providerRef":{"minLength":1,"type":"string"},"request":{"$ref":"#/$defs/ProviderRequest"},"seqBase":{"minimum":0,"type":"integer"},"streamId":{"minLength":1,"type":"string"}},"required":["providerRef","request","seqBase","streamId"],"type":"object"},"StreamOpenResult":{"additionalProperties":false,"properties":{"accepted":{"type":"boolean"}},"required":["accepted"],"type":"object"},"ToolCallParams":{"additionalProperties":false,"properties":{"arguments":true,"name":{"minLength":1,"type":"string"},"timeoutMillis":{"minimum":0,"type":"integer"}},"required":["arguments","name","timeoutMillis"],"type":"object"},"ToolCallResult":{"additionalProperties":false,"properties":{"content":{"type":"string"},"isError":{"type":"boolean"}},"required":["content"],"type":"object"},"ToolSpec":{"additionalProperties":false,"properties":{"description":{"minLength":1,"type":"string"},"inputSchema":true,"name":{"minLength":1,"type":"string"},"readOnly":{"type":"boolean"}},"required":["description","inputSchema","name"],"type":"object"},"UIActionDecl":{"additionalProperties":false,"properties":{"actionId":{"minLength":1,"type":"string"},"label":{"type":"string"}},"required":["actionId"],"type":"object"},"UIActionParams":{"additionalProperties":false,"properties":{"actionId":{"minLength":1,"type":"string"},"args":{"additionalProperties":{"type":"string"},"type":"object"},"generation":{"type":"integer"},"sessionId":{"minLength":1,"type":"string"}},"required":["actionId","generation","sessionId"],"type":"object"},"UIActionResult":{"additionalProperties":false,"properties":{"accepted":{"type":"boolean"},"message":{"type":"string"}},"required":["accepted"],"type":"object"},"UIPublishParams":{"additionalProperties":false,"properties":{"generation":{"type":"integer"},"kind":{"enum":["card","form","notification","panel","status","view"],"type":"string"},"payload":true,"sessionId":{"minLength":1,"type":"string"},"surfaceId":{"minLength":1,"type":"string"}},"required":["generation","kind","payload","sessionId","surfaceId"],"type":"object"},"UIPublishResult":{"additionalProperties":false,"properties":{"accepted":{"type":"boolean"}},"required":["accepted"],"type":"object"},"UIRequestParams":{"additionalProperties":false,"properties":{"generation":{"type":"integer"},"kind":{"enum":["confirm","input","multiselect","select"],"type":"string"},"payload":true,"sessionId":{"minLength":1,"type":"string"},"surfaceId":{"minLength":1,"type":"string"}},"required":["generation","kind","payload","sessionId","surfaceId"],"type":"object"},"UIRequestResult":{"additionalProperties":false,"properties":{"cancelled":{"type":"boolean"},"values":{"additionalProperties":true,"type":"object"}},"required":["cancelled"],"type":"object"},"UISubmitParams":{"additionalProperties":false,"properties":{"generation":{"type":"integer"},"sessionId":{"minLength":1,"type":"string"},"surfaceId":{"minLength":1,"type":"string"},"values":{"additionalProperties":true,"type":"object"}},"required":["generation","sessionId","surfaceId","values"],"type":"object"},"UISubmitResult":{"additionalProperties":false,"properties":{"accepted":{"type":"boolean"}},"required":["accepted"],"type":"object"}},"$id":"reasonix.extension.v2","$schema":"https://json-schema.org/draft/2020-12/schema","errors":[{"jsonRpcCode":-32000,"message":"Component activation failed; the new generation was not published.","reason":"activation_failed","retryable":false},{"jsonRpcCode":-32000,"message":"The extension used a capability its manifest did not declare.","reason":"capability_not_declared","retryable":false},{"jsonRpcCode":-32000,"message":"Component cleanup failed while disposing scoped effects.","reason":"cleanup_failed","retryable":true},{"jsonRpcCode":-32000,"message":"The referenced content has expired.","reason":"content_ref_expired","retryable":true},{"jsonRpcCode":-32000,"message":"The dependency graph contains a required cycle.","reason":"dependency_cycle","retryable":false},{"jsonRpcCode":-32000,"message":"A required dependency is missing or not version-compatible.","reason":"dependency_unsatisfied","retryable":false},{"jsonRpcCode":-32000,"message":"The frame exceeds the frozen protocol size limit.","reason":"frame_too_large","retryable":false},{"jsonRpcCode":-32000,"message":"The extension did not answer an intercept within its timeout.","reason":"intercept_timeout","retryable":true},{"jsonRpcCode":-32603,"message":"An internal extension protocol error occurred.","reason":"internal","retryable":true},{"jsonRpcCode":-32602,"message":"The params do not match the registered method schema.","reason":"invalid_params","retryable":false},{"jsonRpcCode":-32600,"message":"The extension protocol frame or envelope is invalid.","reason":"protocol_error","retryable":false},{"jsonRpcCode":-32000,"message":"The extension provider stream failed.","reason":"provider_failed","retryable":true},{"jsonRpcCode":-32000,"message":"The extension provider stream was interrupted.","reason":"provider_interrupted","retryable":true},{"jsonRpcCode":-32000,"message":"A capability schema hash does not match the expected pin.","reason":"schema_mismatch","retryable":false},{"jsonRpcCode":-32000,"message":"The extension did not shut down within the requested timeout.","reason":"shutdown_timeout","retryable":true},{"jsonRpcCode":-32000,"message":"The message belongs to a superseded runtime generation.","reason":"stale_generation","retryable":false},{"jsonRpcCode":-32000,"message":"The provider stream was cancelled.","reason":"stream_cancelled","retryable":false},{"jsonRpcCode":-32000,"message":"A provider stream chunk is missing from the ordered sequence.","reason":"stream_gap","retryable":true},{"jsonRpcCode":-32601,"message":"The method is not registered in Extension Protocol v2.","reason":"unknown_method","retryable":false},{"jsonRpcCode":-32000,"message":"The peer's extension protocol version is not supported.","reason":"unsupported_version","retryable":false}],"interceptEvents":["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"],"limits":{"contentRefChunkBytes":262144,"contentRefObjectBytes":8388608,"externalizeFieldBytes":65536,"frameBytes":8388608},"methods":{"extension/event":{"class":"observation","direction":"host_to_extension_notification","notification":true,"params":{"$ref":"#/$defs/EventParams"},"result":null},"extension/initialize":{"class":"lifecycle","direction":"host_to_extension_request","notification":false,"params":{"$ref":"#/$defs/InitializeParams"},"result":{"$ref":"#/$defs/InitializeResult"}},"extension/initialized":{"class":"lifecycle","direction":"host_to_extension_notification","notification":true,"params":{"$ref":"#/$defs/InitializedParams"},"result":null},"extension/intercept":{"class":"intercept","direction":"host_to_extension_request","notification":false,"params":{"$ref":"#/$defs/InterceptParams"},"result":{"$ref":"#/$defs/InterceptResult"}},"extension/provider/catalog":{"class":"provider","direction":"host_to_extension_request","notification":false,"params":{"$ref":"#/$defs/ProviderCatalogParams"},"result":{"$ref":"#/$defs/ProviderCatalogResult"}},"extension/provider/stream/cancel":{"class":"provider","direction":"host_to_extension_request","notification":false,"params":{"$ref":"#/$defs/StreamCancelParams"},"result":{"$ref":"#/$defs/StreamCancelResult"}},"extension/provider/stream/chunk":{"class":"provider","direction":"extension_to_host_notification","notification":true,"params":{"$ref":"#/$defs/StreamChunkParams"},"result":null},"extension/provider/stream/end":{"class":"provider","direction":"extension_to_host_notification","notification":true,"params":{"$ref":"#/$defs/StreamEndParams"},"result":null},"extension/provider/stream/open":{"class":"provider","direction":"host_to_extension_request","notification":false,"params":{"$ref":"#/$defs/StreamOpenParams"},"result":{"$ref":"#/$defs/StreamOpenResult"}},"extension/resources/changed":{"class":"observation","direction":"host_to_extension_notification","notification":true,"params":{"$ref":"#/$defs/ResourcesChangedParams"},"result":null},"extension/shutdown":{"class":"lifecycle","direction":"host_to_extension_request","notification":false,"params":{"$ref":"#/$defs/ShutdownParams"},"result":{"$ref":"#/$defs/ShutdownResult"}},"extension/tool/call":{"class":"tool","direction":"host_to_extension_request","notification":false,"params":{"$ref":"#/$defs/ToolCallParams"},"result":{"$ref":"#/$defs/ToolCallResult"}},"extension/ui/action":{"class":"ui","direction":"host_to_extension_request","notification":false,"params":{"$ref":"#/$defs/UIActionParams"},"result":{"$ref":"#/$defs/UIActionResult"}},"extension/ui/submit":{"class":"ui","direction":"host_to_extension_request","notification":false,"params":{"$ref":"#/$defs/UISubmitParams"},"result":{"$ref":"#/$defs/UISubmitResult"}},"host/content/read":{"class":"content","direction":"extension_to_host_request","notification":false,"params":{"$ref":"#/$defs/ContentReadParams"},"result":{"$ref":"#/$defs/ContentReadResult"}},"host/ui/publish":{"class":"ui","direction":"extension_to_host_request","notification":false,"params":{"$ref":"#/$defs/UIPublishParams"},"result":{"$ref":"#/$defs/UIPublishResult"}},"host/ui/request":{"class":"ui","direction":"extension_to_host_request","notification":false,"params":{"$ref":"#/$defs/UIRequestParams"},"result":{"$ref":"#/$defs/UIRequestResult"}}},"protocol":"reasonix.extension.v2","protocolID":"reasonix.extension.v2","protocolMajor":2,"title":"Reasonix Extension Protocol v2"} \ No newline at end of file diff --git a/internal/ext/extension/protocol/schema_hash.generated.go b/internal/ext/extension/protocol/schema_hash.generated.go index ff74da8733..45bb554534 100644 --- a/internal/ext/extension/protocol/schema_hash.generated.go +++ b/internal/ext/extension/protocol/schema_hash.generated.go @@ -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" diff --git a/internal/ext/extension/protocol/validate.go b/internal/ext/extension/protocol/validate.go index a58148c96c..274236d3fb 100644 --- a/internal/ext/extension/protocol/validate.go +++ b/internal/ext/extension/protocol/validate.go @@ -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), diff --git a/internal/runtime/agent/extensions.go b/internal/runtime/agent/extensions.go index b3d93ba123..4fa5fe6c39 100644 --- a/internal/runtime/agent/extensions.go +++ b/internal/runtime/agent/extensions.go @@ -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 } diff --git a/internal/safety/permission/verdict.go b/internal/safety/permission/verdict.go index 84f5bec9d1..a96a9e06b9 100644 --- a/internal/safety/permission/verdict.go +++ b/internal/safety/permission/verdict.go @@ -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) diff --git a/internal/session/control/approval.go b/internal/session/control/approval.go index a9b9892048..b908eb1883 100644 --- a/internal/session/control/approval.go +++ b/internal/session/control/approval.go @@ -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 diff --git a/sdk/go/types_generated.go b/sdk/go/types_generated.go index dc691773db..474550cc60 100644 --- a/sdk/go/types_generated.go +++ b/sdk/go/types_generated.go @@ -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.