源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
This commit is contained in:
commit
2ed3415fb7
8018 files changed
+1936476
No files matched your search
@@ -0,0 +1,230 @@
|
||||
package agentruntime
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
activityshared "github.com/tutti-os/tutti/packages/agent/daemon/activity/events"
|
||||
)
|
||||
|
||||
const (
|
||||
appServerMCPElicitationApprovalKindKey = "codex_approval_kind"
|
||||
appServerMCPElicitationToolApprovalKind = "mcp_tool_call"
|
||||
appServerMCPElicitationToolSuggestKind = "tool_suggestion"
|
||||
appServerMCPElicitationPersistKey = "persist"
|
||||
appServerMCPElicitationPersistSession = "session"
|
||||
appServerMCPElicitationPersistAlways = "always"
|
||||
)
|
||||
|
||||
func (a *CodexAppServerAdapter) appServerMCPElicitationRequested(
|
||||
session Session,
|
||||
turnID string,
|
||||
requestID string,
|
||||
params map[string]any,
|
||||
) ([]activityshared.Event, *pendingInteractiveRequest, error) {
|
||||
if err := validateMessageOnlyMCPElicitation(params); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
message, ok := params["message"].(string)
|
||||
if !ok {
|
||||
return nil, nil, errors.New("MCP elicitation message must be a string")
|
||||
}
|
||||
message = strings.TrimSpace(message)
|
||||
if message == "" {
|
||||
message = "MCP server requests permission."
|
||||
}
|
||||
serverName, ok := params["serverName"].(string)
|
||||
if !ok {
|
||||
return nil, nil, errors.New("MCP elicitation serverName must be a string")
|
||||
}
|
||||
serverName = strings.TrimSpace(serverName)
|
||||
if serverName == "" {
|
||||
return nil, nil, errors.New("MCP elicitation serverName is required")
|
||||
}
|
||||
meta := payloadObject(params["_meta"])
|
||||
if asString(meta[appServerMCPElicitationApprovalKindKey]) == appServerMCPElicitationToolSuggestKind {
|
||||
return nil, nil, errors.New("MCP tool-suggestion elicitation is not supported")
|
||||
}
|
||||
options := appServerMCPElicitationApprovalOptions(meta)
|
||||
input := map[string]any{
|
||||
"requestId": requestID,
|
||||
"reason": message,
|
||||
"serverName": serverName,
|
||||
"mode": "form",
|
||||
"requestedSchema": clonePayloadValue(params["requestedSchema"]),
|
||||
"options": cloneOptionMaps(options),
|
||||
}
|
||||
pending := &pendingInteractiveRequest{
|
||||
agentSessionID: strings.TrimSpace(session.AgentSessionID),
|
||||
turnID: strings.TrimSpace(turnID),
|
||||
requestID: requestID,
|
||||
eventID: newID(),
|
||||
callID: requestID,
|
||||
callType: "approval",
|
||||
input: input,
|
||||
kind: "approval",
|
||||
providerTurnID: strings.TrimSpace(
|
||||
asString(params["turnId"]),
|
||||
),
|
||||
name: message,
|
||||
toolName: serverName,
|
||||
options: options,
|
||||
response: make(chan pendingInteractiveResponse, 1),
|
||||
}
|
||||
a.storePendingRequest(pending)
|
||||
return []activityshared.Event{
|
||||
newTurnActivityEvent(session, EventTurnUpdated, turnID, SessionStatusWaiting, "", "", map[string]any{
|
||||
"phase": string(activityshared.TurnPhaseWaitingApproval),
|
||||
"requestId": requestID,
|
||||
}),
|
||||
newTurnActivityEventWithID(
|
||||
session,
|
||||
pending.eventID,
|
||||
EventCallStarted,
|
||||
turnID,
|
||||
SessionStatusWaiting,
|
||||
"",
|
||||
message,
|
||||
map[string]any{
|
||||
"callId": pending.callID,
|
||||
"callType": pending.callType,
|
||||
"name": pending.name,
|
||||
"toolName": pending.toolName,
|
||||
"status": string(activityshared.TurnPhaseWaitingApproval),
|
||||
"input": clonePayload(input),
|
||||
"metadata": map[string]any{
|
||||
"callType": pending.callType,
|
||||
"mcpServer": serverName,
|
||||
},
|
||||
},
|
||||
),
|
||||
normalizedInteractionRequestedEvent(session, turnID, pending),
|
||||
}, pending, nil
|
||||
}
|
||||
|
||||
func validateMessageOnlyMCPElicitation(params map[string]any) error {
|
||||
mode := strings.TrimSpace(asString(params["mode"]))
|
||||
if mode != "form" {
|
||||
return fmt.Errorf("MCP elicitation mode %q is not supported", mode)
|
||||
}
|
||||
schema, ok := params["requestedSchema"].(map[string]any)
|
||||
if !ok {
|
||||
return errors.New("MCP elicitation requestedSchema must be an object")
|
||||
}
|
||||
if strings.TrimSpace(asString(schema["type"])) != "object" {
|
||||
return errors.New("MCP elicitation requestedSchema type must be object")
|
||||
}
|
||||
properties, ok := schema["properties"].(map[string]any)
|
||||
if !ok {
|
||||
return errors.New("MCP elicitation requestedSchema properties must be an object")
|
||||
}
|
||||
if len(properties) > 0 {
|
||||
return errors.New("MCP elicitation forms with fields are not supported")
|
||||
}
|
||||
if required, exists := schema["required"]; exists {
|
||||
values, valid := appServerMCPStringArray(required)
|
||||
if !valid {
|
||||
return errors.New("MCP elicitation requestedSchema required must be a string array")
|
||||
}
|
||||
if len(values) > 0 {
|
||||
return errors.New("MCP elicitation message-only schema cannot require fields")
|
||||
}
|
||||
}
|
||||
for key := range schema {
|
||||
switch key {
|
||||
case "$schema", "type", "properties", "required":
|
||||
default:
|
||||
return fmt.Errorf("MCP elicitation requestedSchema field %q is not supported", key)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func appServerMCPElicitationApprovalOptions(meta map[string]any) []map[string]any {
|
||||
options := []map[string]any{
|
||||
{"optionId": "approve", "name": "Allow", "kind": "allow_once"},
|
||||
}
|
||||
if appServerMCPElicitationSupportsPersist(meta, appServerMCPElicitationPersistSession) {
|
||||
options = append(options, map[string]any{
|
||||
"optionId": "approve_for_session",
|
||||
"name": "Allow for this session",
|
||||
"kind": "allow_always",
|
||||
})
|
||||
}
|
||||
if appServerMCPElicitationSupportsPersist(meta, appServerMCPElicitationPersistAlways) {
|
||||
options = append(options, map[string]any{
|
||||
"optionId": "approve_always",
|
||||
"name": "Always allow",
|
||||
"kind": "allow_always",
|
||||
})
|
||||
}
|
||||
if asString(meta[appServerMCPElicitationApprovalKindKey]) == appServerMCPElicitationToolApprovalKind {
|
||||
return append(options, map[string]any{
|
||||
"optionId": "cancel",
|
||||
"name": "Cancel",
|
||||
"kind": "reject_once",
|
||||
})
|
||||
}
|
||||
return append(options,
|
||||
map[string]any{"optionId": "deny", "name": "Deny", "kind": "reject_once"},
|
||||
map[string]any{"optionId": "cancel", "name": "Cancel", "kind": "reject_once"},
|
||||
)
|
||||
}
|
||||
|
||||
func appServerMCPElicitationSupportsPersist(meta map[string]any, expected string) bool {
|
||||
persist := meta[appServerMCPElicitationPersistKey]
|
||||
if asString(persist) == expected {
|
||||
return true
|
||||
}
|
||||
values, _ := appServerMCPStringArray(persist)
|
||||
for _, value := range values {
|
||||
if value == expected {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func appServerMCPStringArray(value any) ([]string, bool) {
|
||||
switch typed := value.(type) {
|
||||
case nil:
|
||||
return nil, true
|
||||
case []string:
|
||||
return append([]string(nil), typed...), true
|
||||
case []any:
|
||||
values := make([]string, 0, len(typed))
|
||||
for _, entry := range typed {
|
||||
text, ok := entry.(string)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
values = append(values, text)
|
||||
}
|
||||
return values, true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
func appServerMCPElicitationResult(selection pendingInteractiveResponse) map[string]any {
|
||||
result := map[string]any{}
|
||||
switch strings.TrimSpace(selection.optionID) {
|
||||
case "approve":
|
||||
result["action"] = "accept"
|
||||
case "approve_for_session":
|
||||
result["action"] = "accept"
|
||||
result["_meta"] = map[string]any{appServerMCPElicitationPersistKey: appServerMCPElicitationPersistSession}
|
||||
case "approve_always":
|
||||
result["action"] = "accept"
|
||||
result["_meta"] = map[string]any{appServerMCPElicitationPersistKey: appServerMCPElicitationPersistAlways}
|
||||
case "deny":
|
||||
result["action"] = "decline"
|
||||
case "cancel":
|
||||
result["action"] = "cancel"
|
||||
default:
|
||||
result["action"] = "cancel"
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in new issue
Block a user