package protocol import ( "context" "encoding/json" "fmt" "net/http" "net/url" "strconv" "strings" ) type ManifestOperation struct { Method string `json:"method"` Path string `json:"path"` ContentType string `json:"contentType,omitempty"` Fields map[string]string `json:"fields,omitempty"` } type ManifestResponse struct { TaskIDPaths []string `json:"taskIdPaths,omitempty"` StatusPaths []string `json:"statusPaths,omitempty"` ErrorPaths []string `json:"errorPaths,omitempty"` MessagePaths []string `json:"messagePaths,omitempty"` TextPaths []string `json:"textPaths,omitempty"` ReasoningPaths []string `json:"reasoningPaths,omitempty"` ResultURLPaths []string `json:"resultUrlPaths,omitempty"` ResultPaths []string `json:"resultPaths,omitempty"` ResultKind string `json:"resultKind,omitempty"` ResultEphemeral bool `json:"resultEphemeral,omitempty"` } // ManifestAgentResponse describes the provider response shape for a // tool-capable text request. Tool call paths are resolved against each item in // ToolCallsPath. type ManifestAgentResponse struct { TextPaths []string `json:"textPaths,omitempty"` ReasoningPaths []string `json:"reasoningPaths,omitempty"` ToolCallsPath string `json:"toolCallsPath,omitempty"` ToolCallIDPaths []string `json:"toolCallIdPaths,omitempty"` ToolCallNamePaths []string `json:"toolCallNamePaths,omitempty"` ToolCallArgsPaths []string `json:"toolCallArgumentsPaths,omitempty"` } // AdapterResolver is used by the host to bind a shipped execution engine to a // manifest. Uploaded plugins must use the declarative path; host bindings are // reserved for manifests shipped with the application. type AdapterResolver func(string) (Adapter, bool) func LoadInstalledPlugin(data []byte, resolve AdapterResolver) (Adapter, error) { adapters, err := LoadInstalledProviders(data, resolve) if err != nil { return nil, err } if len(adapters) == 0 { return nil, fmt.Errorf("plugin does not provide an executable provider") } return adapters[0], nil } // LoadInstalledProviders loads every provider contribution from one plugin // package. The package remains the lifecycle and permission unit; providers // are separate execution entries in the runtime index. func LoadInstalledProviders(data []byte, resolve AdapterResolver) ([]Adapter, error) { manifest, err := decodeManifest(data) if err != nil { return nil, err } if strings.HasPrefix(strings.TrimSpace(manifest.Runtime.Backend), "host:") { if len(manifest.Contributes.Providers) != 1 { return nil, fmt.Errorf("host-backed plugin must declare exactly one provider") } if resolve == nil { return nil, fmt.Errorf("plugin %q requires a host execution engine", manifest.Metadata.ID) } name := strings.TrimPrefix(strings.TrimSpace(manifest.Runtime.Backend), "host:") adapter, ok := resolve(name) if !ok { return nil, fmt.Errorf("plugin host execution %q is unavailable", name) } if err := ValidateManifest(manifest); err != nil { return nil, err } if err := normalizeManifest(&manifest); err != nil { return nil, err } return []Adapter{metadataAdapter{metadata: manifest.Metadata, delegate: adapter}}, nil } if len(manifest.Contributes.Providers) == 0 { adapter, err := loadDeclarativeManifest(manifest) if err != nil { return nil, err } return []Adapter{adapter}, nil } result := make([]Adapter, 0, len(manifest.Contributes.Providers)) for index := range manifest.Contributes.Providers { adapter, err := loadDeclarativeManifestProvider(manifest, index) if err != nil { return nil, err } result = append(result, adapter) } return result, nil } func validatePluginMetadata(metadata Metadata) error { if strings.TrimSpace(metadata.ID) == "" || strings.TrimSpace(metadata.Version) == "" || !validManifestIdentifier(metadata.ID) { return fmt.Errorf("protocol plugin metadata is invalid") } if len(metadata.Categories) == 0 || len(metadata.Scopes) == 0 { return fmt.Errorf("protocol plugin metadata requires categories and scopes") } for _, capability := range metadata.Categories { if capability != CapabilityText && capability != CapabilityImage && capability != CapabilityVideo && capability != CapabilityAudio { return fmt.Errorf("unsupported protocol capability %q", capability) } } for _, scope := range metadata.Scopes { switch scope { case SurfaceAdminSystemChannel, SurfaceUserCustomChannel, SurfaceCanvas, SurfaceCreation, SurfaceAgent: default: return fmt.Errorf("unsupported protocol scope %q", scope) } } return nil } type metadataAdapter struct { metadata Metadata delegate Adapter } func (a metadataAdapter) Metadata() Metadata { return a.metadata } func (a metadataAdapter) BuildCreate(ctx context.Context, c RequestContext) (RequestSpec, error) { return a.delegate.BuildCreate(ctx, c) } func (a metadataAdapter) ParseCreate(ctx context.Context, body []byte) (CreateResult, error) { return a.delegate.ParseCreate(ctx, body) } func (a metadataAdapter) BuildPoll(ctx context.Context, c PollContext) (RequestSpec, error) { return a.delegate.BuildPoll(ctx, c) } func (a metadataAdapter) ParsePoll(ctx context.Context, c PollContext, body []byte) (PollResult, error) { return a.delegate.ParsePoll(ctx, c, body) } func (a metadataAdapter) BuildCancel(ctx context.Context, c PollContext) (RequestSpec, error) { return a.delegate.BuildCancel(ctx, c) } func (a metadataAdapter) BuildAgent(ctx context.Context, c AgentRequestContext) (RequestSpec, error) { adapter, ok := a.delegate.(AgentAdapter) if !ok { return RequestSpec{}, unavailable(a.metadata) } return adapter.BuildAgent(ctx, c) } func (a metadataAdapter) ParseAgent(ctx context.Context, body []byte) (AgentResult, error) { adapter, ok := a.delegate.(AgentAdapter) if !ok { return AgentResult{}, unavailable(a.metadata) } return adapter.ParseAgent(ctx, body) } func LoadManifest(data []byte) (Adapter, error) { manifest, err := decodeManifest(data) if err != nil { return nil, err } return loadDeclarativeManifest(manifest) } func decodeManifest(data []byte) (Manifest, error) { var manifest Manifest if err := json.Unmarshal(data, &manifest); err != nil { return Manifest{}, fmt.Errorf("decode plugin manifest: %w", err) } if err := ValidateManifest(manifest); err != nil { return Manifest{}, err } return manifest, nil } func loadDeclarativeManifest(manifest Manifest) (Adapter, error) { manifest.Runtime.Backend = "declarative" if err := normalizeManifest(&manifest); err != nil { return nil, err } return manifestAdapter{manifest: manifest}, nil } func loadDeclarativeManifestProvider(manifest Manifest, index int) (Adapter, error) { manifest.Runtime.Backend = "declarative" if err := normalizeManifestForProvider(&manifest, index); err != nil { return nil, err } return manifestAdapter{manifest: manifest}, nil } func ValidateManifest(manifest Manifest) error { if strings.TrimSpace(manifest.APIVersion) != "yingce.plugin/v1" { return fmt.Errorf("unsupported protocol manifest apiVersion %q", manifest.APIVersion) } if strings.TrimSpace(manifest.Metadata.ID) == "" || strings.TrimSpace(manifest.Metadata.Version) == "" { return fmt.Errorf("protocol manifest metadata requires id and version") } if strings.TrimSpace(manifest.Metadata.Name) == "" { return fmt.Errorf("protocol manifest metadata requires name") } if !validManifestIdentifier(manifest.Metadata.ID) { return fmt.Errorf("protocol manifest metadata id is invalid") } if len(manifest.Metadata.Name) > 160 || len(manifest.Metadata.Vendor) > 120 { return fmt.Errorf("protocol manifest metadata name or vendor is too long") } if len(manifest.Contributes.Providers) == 0 && !hasNonProviderContribution(manifest.Contributes) { return fmt.Errorf("plugin must declare at least one contribution") } if len(manifest.Contributes.Providers) == 0 { return nil } provider := manifest.Contributes.Providers[0] if strings.TrimSpace(provider.ID) == "" || strings.TrimSpace(provider.Label) == "" { return fmt.Errorf("provider contribution requires id and label") } if len(provider.Capabilities) == 0 || len(provider.Scopes) == 0 { return fmt.Errorf("provider contribution requires capabilities and scopes") } for _, capability := range provider.Capabilities { if capability != CapabilityText && capability != CapabilityImage && capability != CapabilityVideo && capability != CapabilityAudio { return fmt.Errorf("unsupported protocol capability %q", capability) } } for _, scope := range provider.Scopes { switch scope { case SurfaceAdminSystemChannel, SurfaceUserCustomChannel, SurfaceCanvas, SurfaceCreation, SurfaceAgent: default: return fmt.Errorf("unsupported protocol scope %q", scope) } } if err := validateManifestOperation(provider.Create); err != nil { return fmt.Errorf("create operation: %w", err) } if provider.Agent != nil { if err := validateManifestOperation(*provider.Agent); err != nil { return fmt.Errorf("agent operation: %w", err) } if provider.AgentResponse == nil { return fmt.Errorf("agent response mapping is required when agent operation is declared") } } if provider.Poll != nil { if err := validateManifestOperation(*provider.Poll); err != nil { return fmt.Errorf("poll operation: %w", err) } } if provider.Cancel != nil { if err := validateManifestOperation(*provider.Cancel); err != nil { return fmt.Errorf("cancel operation: %w", err) } } return nil } func normalizeManifest(manifest *Manifest) error { if manifest == nil { return fmt.Errorf("plugin manifest is missing") } if len(manifest.Contributes.Providers) == 0 { manifest.Metadata.Execution = manifest.Runtime.Backend return nil } return normalizeManifestForProvider(manifest, 0) } func normalizeManifestForProvider(manifest *Manifest, index int) error { if manifest == nil || index < 0 || index >= len(manifest.Contributes.Providers) { return fmt.Errorf("plugin provider contribution is missing") } provider := manifest.Contributes.Providers[index] manifest.Metadata.ID = provider.ID manifest.Metadata.Categories = provider.Capabilities manifest.Metadata.Scopes = provider.Scopes manifest.Metadata.Parameters = provider.Parameters manifest.Metadata.Create = operationSummary(provider.Create) manifest.Metadata.Poll = operationSummaryPtr(provider.Poll) manifest.Metadata.Cancel = operationSummaryPtr(provider.Cancel) manifest.Metadata.ContentType = provider.Create.ContentType manifest.Metadata.RequiresPublicMediaURLs = provider.RequiresPublicMediaURLs manifest.Metadata.Execution = manifest.Runtime.Backend manifest.Create = provider.Create manifest.Agent = provider.Agent manifest.Poll = provider.Poll manifest.Cancel = provider.Cancel manifest.Response = provider.Response manifest.AgentResponse = provider.AgentResponse return nil } func hasNonProviderContribution(contributes ManifestContributions) bool { return len(contributes.Workflows) > 0 || len(contributes.CanvasNodes) > 0 || len(contributes.Transforms) > 0 || len(contributes.Commands) > 0 || len(contributes.AssetSources) > 0 || len(contributes.UsageObservers) > 0 || len(contributes.AICapabilities) > 0 || len(contributes.Agents) > 0 || len(contributes.ImportExport) > 0 } func operationSummary(operation ManifestOperation) string { path := strings.ReplaceAll(operation.Path, "{{model}}", "{model}") path = strings.ReplaceAll(path, "{{taskId}}", "{task_id}") return strings.ToUpper(operation.Method) + " " + path } func operationSummaryPtr(operation *ManifestOperation) string { if operation == nil { return "" } return operationSummary(*operation) } func validateManifestOperation(operation ManifestOperation) error { method := strings.ToUpper(strings.TrimSpace(operation.Method)) if method != http.MethodGet && method != http.MethodPost && method != http.MethodDelete && method != http.MethodPut { return fmt.Errorf("unsupported HTTP method %q", operation.Method) } if !isRelativePath(operation.Path) { return fmt.Errorf("path must be relative: %q", operation.Path) } return nil } type manifestAdapter struct{ manifest Manifest } func (a manifestAdapter) Metadata() Metadata { return a.manifest.Metadata } func (a manifestAdapter) BuildCreate(_ context.Context, c RequestContext) (RequestSpec, error) { if len(a.manifest.Contributes.Providers) == 0 { return RequestSpec{}, fmt.Errorf("plugin %s does not provide a provider", a.manifest.Metadata.ID) } return buildManifestOperation(a.manifest.Create, c.Request, ""), nil } func (a manifestAdapter) BuildAgent(_ context.Context, c AgentRequestContext) (RequestSpec, error) { if a.manifest.Agent == nil { return RequestSpec{}, fmt.Errorf("protocol %s has no agent operation", a.manifest.Metadata.ID) } request := GenerationRequest{Model: c.Model, Extra: map[string]any{"agent": c.Request}} return buildManifestOperation(*a.manifest.Agent, request, ""), nil } func (a manifestAdapter) ParseAgent(_ context.Context, body []byte) (AgentResult, error) { if a.manifest.AgentResponse == nil { return AgentResult{}, fmt.Errorf("protocol %s has no agent response mapping", a.manifest.Metadata.ID) } payload, err := decodeObject(body) if err != nil { return AgentResult{}, err } response := a.manifest.AgentResponse result := AgentResult{Text: firstPathValue(payload, response.TextPaths...), Reasoning: firstPathValue(payload, response.ReasoningPaths...)} if response.ToolCallsPath == "" { return result, nil } for _, item := range arrayValue(pathValue(payload, response.ToolCallsPath)) { object, ok := item.(map[string]any) if !ok { continue } result.ToolCalls = append(result.ToolCalls, AgentToolCall{ ID: firstPathValue(object, response.ToolCallIDPaths...), Name: firstPathValue(object, response.ToolCallNamePaths...), Arguments: firstPathJSONValue(object, response.ToolCallArgsPaths...), }) } return result, nil } func (a manifestAdapter) ParseCreate(_ context.Context, body []byte) (CreateResult, error) { payload, err := decodeObject(body) if err != nil { return CreateResult{}, err } return a.parse(payload, PollContext{}), nil } func (a manifestAdapter) BuildPoll(_ context.Context, c PollContext) (RequestSpec, error) { if a.manifest.Poll == nil { return RequestSpec{}, fmt.Errorf("protocol %s has no poll operation", a.manifest.Metadata.ID) } request := c.Request request.Model = c.Model return buildManifestOperation(*a.manifest.Poll, request, c.TaskID), nil } func (a manifestAdapter) ParsePoll(_ context.Context, c PollContext, body []byte) (PollResult, error) { payload, err := decodeObject(body) if err != nil { return PollResult{}, err } result := a.parse(payload, c) return PollResult{TaskID: result.TaskID, Status: result.Status, Result: result.Result, Message: result.Message}, nil } func (a manifestAdapter) BuildCancel(_ context.Context, c PollContext) (RequestSpec, error) { if a.manifest.Cancel == nil { return RequestSpec{}, fmt.Errorf("protocol %s does not support cancellation", a.manifest.Metadata.ID) } request := c.Request request.Model = c.Model return buildManifestOperation(*a.manifest.Cancel, request, c.TaskID), nil } func (a manifestAdapter) parse(payload map[string]any, c PollContext) CreateResult { id := firstPathValue(payload, a.manifest.Response.TaskIDPaths...) if id == "" { id = c.TaskID } status := normalizeStatus(firstPathValue(payload, a.manifest.Response.StatusPaths...)) if status == "" { status = StatusPending } message := firstPathValue(payload, a.manifest.Response.MessagePaths...) if manifestError(payload, a.manifest.Response.ErrorPaths...) { status = StatusFailed } result := &Result{ Text: firstPathValue(payload, a.manifest.Response.TextPaths...), Reasoning: firstPathValue(payload, a.manifest.Response.ReasoningPaths...), } paths := append([]string{}, a.manifest.Response.ResultURLPaths...) paths = append(paths, a.manifest.Response.ResultPaths...) for _, path := range paths { for _, value := range mediaPathValues(payload, path) { item := MediaReference{URL: value, Kind: a.manifest.Response.ResultKind, Ephemeral: a.manifest.Response.ResultEphemeral} switch a.manifest.Response.ResultKind { case "image": result.Images = append(result.Images, item) case "audio": result.Audios = append(result.Audios, item) default: result.Videos = append(result.Videos, item) } } } if status == StatusPending && (result.Text != "" || len(result.Images) > 0 || len(result.Videos) > 0 || len(result.Audios) > 0) { status = StatusSucceeded } if result.Text == "" && result.Reasoning == "" && len(result.Images) == 0 && len(result.Videos) == 0 && len(result.Audios) == 0 { result = nil } return CreateResult{TaskID: id, Status: status, Result: result, Message: message} } func mediaPathValues(payload map[string]any, path string) []string { value := pathValue(payload, path) if text, ok := value.(string); ok && strings.TrimSpace(text) != "" { return []string{strings.TrimSpace(text)} } items, ok := value.([]any) if !ok { return nil } values := make([]string, 0, len(items)) for _, item := range items { switch typed := item.(type) { case string: if strings.TrimSpace(typed) != "" { values = append(values, strings.TrimSpace(typed)) } case map[string]any: if url := firstString(typed, "url", "file_url", "fileUrl", "video_url", "videoUrl", "image_url", "imageUrl"); url != "" { values = append(values, url) } } } return values } func buildManifestOperation(operation ManifestOperation, request GenerationRequest, taskID string) RequestSpec { var body map[string]any if len(operation.Fields) > 0 { body = make(map[string]any, len(operation.Fields)) } for key, expression := range operation.Fields { value := manifestExpressionValue(expression, request, taskID) if value == nil { continue } setMapPath(body, key, value) } path := strings.ReplaceAll(operation.Path, "{{taskId}}", url.PathEscape(taskID)) path = strings.ReplaceAll(path, "{{model}}", url.PathEscape(request.Model)) return RequestSpec{Method: strings.ToUpper(operation.Method), Path: path, ContentType: defaultValue(operation.ContentType, "application/json"), Body: body} } func manifestExpressionValue(expression string, request GenerationRequest, taskID string) any { expression = strings.TrimSpace(expression) source := expression transforms := []string{} if separator := strings.IndexByte(expression, '|'); separator >= 0 { source = strings.TrimSpace(expression[:separator]) transforms = strings.Split(expression[separator+1:], "|") } var value any switch { case source == "taskId": value = taskID case strings.HasPrefix(source, "request."): value = pathValue(manifestRequestValues(request), strings.TrimPrefix(source, "request.")) default: value = source } for _, transform := range transforms { value = applyManifestTransform(value, transform) } return value } // manifestRequestValues is deliberately a small, JSON-shaped view of the // platform request. It lets uploaded manifests address media items by index // without exposing Go structs or adding provider-specific host code. func manifestRequestValues(request GenerationRequest) map[string]any { return map[string]any{ "model": request.Model, "prompt": request.Prompt, "images": manifestMediaValues(request.Images), "videos": manifestMediaValues(request.Videos), "audios": manifestMediaValues(request.Audios), "imageCount": request.ImageCount, "duration": request.Duration, "aspectRatio": request.AspectRatio, "resolution": request.Resolution, "quality": request.Quality, "generateAudio": request.GenerateAudio, "watermark": request.Watermark, "operation": request.Operation, "extra": request.Extra, } } func manifestMediaValues(values []MediaReference) []any { result := make([]any, 0, len(values)) for _, value := range values { result = append(result, map[string]any{ "url": value.URL, "dataUrl": value.DataURL, "kind": value.Kind, "ephemeral": value.Ephemeral, }) } return result } func applyManifestTransform(value any, transform string) any { transform = strings.ToLower(strings.TrimSpace(transform)) text, ok := value.(string) if !ok { return value } switch transform { case "trim": return strings.TrimSpace(text) case "lower", "lowercase": return strings.ToLower(text) case "upper", "uppercase": return strings.ToUpper(text) default: return value } } func pathValue(payload map[string]any, path string) any { var value any = payload for _, part := range strings.Split(strings.Trim(path, "."), ".") { switch current := value.(type) { case map[string]any: value = current[part] case []any: index, err := strconv.Atoi(part) if err != nil || index < 0 || index >= len(current) { return nil } value = current[index] default: return nil } } return value } func arrayValue(value any) []any { items, _ := value.([]any) return items } func firstPathValue(payload map[string]any, paths ...string) string { for _, path := range paths { value := pathValue(payload, path) if text, ok := value.(string); ok && strings.TrimSpace(text) != "" { return strings.TrimSpace(text) } } return "" } func manifestError(payload map[string]any, paths ...string) bool { for _, path := range paths { value := pathValue(payload, path) switch typed := value.(type) { case nil: continue case string: switch strings.ToLower(strings.TrimSpace(typed)) { case "", "0", "ok", "success", "succeeded", "true": continue default: return true } case float64: if typed == 0 { continue } return true case float32: if typed == 0 { continue } return true case int, int8, int16, int32, int64: if reflectValueIsZero(typed) { continue } return true case uint, uint8, uint16, uint32, uint64: if reflectValueIsZero(typed) { continue } return true default: return true } } return false } func reflectValueIsZero(value any) bool { switch typed := value.(type) { case int: return typed == 0 case int8: return typed == 0 case int16: return typed == 0 case int32: return typed == 0 case int64: return typed == 0 case uint: return typed == 0 case uint8: return typed == 0 case uint16: return typed == 0 case uint32: return typed == 0 case uint64: return typed == 0 default: return false } } // firstPathJSONValue keeps scalar response fields string-like while allowing // providers to return tool arguments as an object instead of a JSON string. // The platform tool contract always stores arguments as JSON text. func firstPathJSONValue(payload map[string]any, paths ...string) string { for _, path := range paths { value := pathValue(payload, path) switch typed := value.(type) { case string: if strings.TrimSpace(typed) != "" { return strings.TrimSpace(typed) } case nil: continue default: encoded, err := json.Marshal(typed) if err == nil && len(encoded) > 0 { return string(encoded) } } } return "" } func setMapPath(target map[string]any, path string, value any) { parts := strings.Split(strings.Trim(path, "."), ".") current := target for _, part := range parts[:len(parts)-1] { next, ok := current[part].(map[string]any) if !ok { next = map[string]any{} current[part] = next } current = next } if len(parts) > 0 { current[parts[len(parts)-1]] = value } } func isRelativePath(path string) bool { parsed, err := url.Parse(strings.TrimSpace(path)) return err == nil && parsed.Path != "" && strings.HasPrefix(parsed.Path, "/") && !strings.HasPrefix(parsed.Path, "//") && parsed.Host == "" && parsed.User == nil && parsed.Scheme == "" } func validManifestIdentifier(value string) bool { if len(value) == 0 || len(value) > 96 { return false } for index, char := range value { if (char >= 'a' && char <= 'z') || (char >= '0' && char <= '9') || char == '-' || char == '_' || char == '.' { if index == 0 && (char == '-' || char == '_' || char == '.') { return false } continue } return false } return true }