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
348 lines
8.9 KiB
Go
348 lines
8.9 KiB
Go
package runtimeprep
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
type CommandSourceKind string
|
|
|
|
const (
|
|
CommandSourceBuiltin CommandSourceKind = "builtin"
|
|
CommandSourceApp CommandSourceKind = "app"
|
|
)
|
|
|
|
type CommandSource struct {
|
|
Kind CommandSourceKind
|
|
AppID string
|
|
AppName string
|
|
}
|
|
|
|
type CommandTableColumn struct {
|
|
Key string
|
|
Label string
|
|
}
|
|
|
|
type CommandTableOutput struct {
|
|
Columns []CommandTableColumn
|
|
}
|
|
|
|
type CommandCapabilityOutput struct {
|
|
DefaultMode string
|
|
JSON bool
|
|
Table *CommandTableOutput
|
|
}
|
|
|
|
type CommandCapability struct {
|
|
ID string
|
|
Path []string
|
|
Summary string
|
|
Description string
|
|
Visibility string
|
|
InputSchema map[string]any
|
|
Output CommandCapabilityOutput
|
|
ExecutionMode string
|
|
Source CommandSource
|
|
}
|
|
|
|
type CommandContext struct {
|
|
Source string
|
|
WorkspaceID string
|
|
SkipCapabilityFilters bool
|
|
IncludeIntegrationCapabilities bool
|
|
}
|
|
|
|
type CommandCatalog interface {
|
|
Capabilities(context.Context, CommandContext) []CommandCapability
|
|
}
|
|
|
|
type CommandCapabilityProjection struct {
|
|
// AllowedIDs switches the projection to an exact capability allowlist when
|
|
// non-empty. It includes both public and promoted integration capabilities.
|
|
// Empty preserves the legacy public-by-default projection.
|
|
AllowedIDs []string
|
|
IncludeIntegrationIDs []string
|
|
ExcludeIDs []string
|
|
}
|
|
|
|
func resolveCommandCapabilities(
|
|
ctx context.Context,
|
|
catalog CommandCatalog,
|
|
workspaceID string,
|
|
cliName string,
|
|
projections ...*CommandCapabilityProjection,
|
|
) (*CommandResolver, error) {
|
|
if catalog == nil {
|
|
return nil, errors.New("agent runtime preparation requires a command catalog")
|
|
}
|
|
capabilities := catalog.Capabilities(ctx, CommandContext{
|
|
Source: "agent-runtime",
|
|
WorkspaceID: strings.TrimSpace(workspaceID),
|
|
SkipCapabilityFilters: true,
|
|
IncludeIntegrationCapabilities: projectionRequestsIntegration(
|
|
projections,
|
|
),
|
|
})
|
|
var projection *CommandCapabilityProjection
|
|
if len(projections) > 0 {
|
|
projection = projections[0]
|
|
}
|
|
projected, err := projectCommandCapabilities(capabilities, projection)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return newCommandResolver(cliName, projected)
|
|
}
|
|
|
|
func projectionRequestsIntegration(
|
|
projections []*CommandCapabilityProjection,
|
|
) bool {
|
|
return len(projections) > 0 &&
|
|
projections[0] != nil &&
|
|
len(normalizedCommandCapabilityIDs(
|
|
projections[0].IncludeIntegrationIDs,
|
|
)) > 0
|
|
}
|
|
|
|
func commandVisibleToAgent(capability CommandCapability) bool {
|
|
visibility := strings.TrimSpace(capability.Visibility)
|
|
return visibility == "" || visibility == "public"
|
|
}
|
|
|
|
func projectCommandCapabilities(
|
|
capabilities []CommandCapability,
|
|
projection *CommandCapabilityProjection,
|
|
) ([]CommandCapability, error) {
|
|
if projection == nil {
|
|
return capabilities, nil
|
|
}
|
|
included := normalizedCommandCapabilityIDs(projection.IncludeIntegrationIDs)
|
|
excluded := normalizedCommandCapabilityIDs(projection.ExcludeIDs)
|
|
allowed := normalizedCommandCapabilityIDs(projection.AllowedIDs)
|
|
foundIncluded := make(map[string]struct{}, len(included))
|
|
foundAllowed := make(map[string]struct{}, len(allowed))
|
|
projected := make([]CommandCapability, 0, len(capabilities))
|
|
for _, capability := range capabilities {
|
|
id := strings.TrimSpace(capability.ID)
|
|
if len(allowed) > 0 {
|
|
if _, accepted := allowed[id]; !accepted {
|
|
continue
|
|
}
|
|
}
|
|
if _, denied := excluded[id]; denied {
|
|
continue
|
|
}
|
|
if _, requested := included[id]; requested {
|
|
capability.Visibility = "public"
|
|
foundIncluded[id] = struct{}{}
|
|
}
|
|
projected = append(projected, capability)
|
|
if _, required := allowed[id]; required {
|
|
foundAllowed[id] = struct{}{}
|
|
}
|
|
}
|
|
for id := range allowed {
|
|
if _, found := foundAllowed[id]; !found {
|
|
return nil, fmt.Errorf(
|
|
"agent command capability projection requires unavailable allowed command %q",
|
|
id,
|
|
)
|
|
}
|
|
}
|
|
for id := range included {
|
|
if _, found := foundIncluded[id]; !found {
|
|
return nil, fmt.Errorf(
|
|
"agent command capability projection requires unavailable command %q",
|
|
id,
|
|
)
|
|
}
|
|
}
|
|
return projected, nil
|
|
}
|
|
|
|
func normalizedCommandCapabilityIDs(values []string) map[string]struct{} {
|
|
normalized := make(map[string]struct{}, len(values))
|
|
for _, value := range values {
|
|
if value = strings.TrimSpace(value); value != "" {
|
|
normalized[value] = struct{}{}
|
|
}
|
|
}
|
|
return normalized
|
|
}
|
|
|
|
// Wait execution adds a wrapper-owned timeout input. It is part of the
|
|
// invocation contract even though app handlers do not receive it.
|
|
func executionFacingInputSchema(mode string, schema map[string]any) map[string]any {
|
|
if strings.TrimSpace(mode) != "wait" {
|
|
return schema
|
|
}
|
|
projected := cloneSchemaMap(schema)
|
|
projected["type"] = "object"
|
|
properties := cloneSchemaMap(mapSchemaValue(schema["properties"]))
|
|
if _, exists := properties["timeout-ms"]; !exists {
|
|
properties["timeout-ms"] = map[string]any{
|
|
"type": "integer",
|
|
"description": "Maximum total wait in milliseconds; omit to wait until the command reaches a stop point.",
|
|
}
|
|
}
|
|
projected["properties"] = properties
|
|
return projected
|
|
}
|
|
|
|
func cloneSchemaMap(source map[string]any) map[string]any {
|
|
if source == nil {
|
|
return nil
|
|
}
|
|
clone := make(map[string]any, len(source))
|
|
for key, value := range source {
|
|
clone[key] = cloneSchemaValue(value)
|
|
}
|
|
return clone
|
|
}
|
|
|
|
func cloneSchemaValue(value any) any {
|
|
switch typed := value.(type) {
|
|
case map[string]any:
|
|
return cloneSchemaMap(typed)
|
|
case []any:
|
|
clone := make([]any, len(typed))
|
|
for index, item := range typed {
|
|
clone[index] = cloneSchemaValue(item)
|
|
}
|
|
return clone
|
|
case []map[string]any:
|
|
clone := make([]map[string]any, len(typed))
|
|
for index, item := range typed {
|
|
clone[index] = cloneSchemaMap(item)
|
|
}
|
|
return clone
|
|
case []string:
|
|
return append([]string(nil), typed...)
|
|
default:
|
|
return value
|
|
}
|
|
}
|
|
|
|
func commandPath(path []string) string {
|
|
return strings.Join(normalizedCommandPath(path), " ")
|
|
}
|
|
|
|
func inputDetailsForCommand(_ string, schema map[string]any) string {
|
|
properties := mapSchemaValue(schema["properties"])
|
|
if len(properties) == 0 {
|
|
return ""
|
|
}
|
|
required := make(map[string]bool)
|
|
for _, name := range stringSliceSchemaValue(schema["required"]) {
|
|
required[strings.TrimSpace(name)] = true
|
|
}
|
|
names := make([]string, 0, len(properties))
|
|
for name := range properties {
|
|
if name = strings.TrimSpace(name); name != "" {
|
|
names = append(names, name)
|
|
}
|
|
}
|
|
sortCommandInputs(names, required)
|
|
parts := make([]string, 0, len(names))
|
|
for _, name := range names {
|
|
property := mapSchemaValue(properties[name])
|
|
detail := "--" + name
|
|
if typ := schemaTypeLabel(property); typ != "" {
|
|
detail += " <" + typ + ">"
|
|
}
|
|
qualifiers := []string{"optional"}
|
|
if required[name] {
|
|
qualifiers[0] = "required"
|
|
}
|
|
if enum := stringSliceSchemaValue(property["enum"]); len(enum) > 0 {
|
|
qualifiers = append(qualifiers, "values: "+strings.Join(enum, "|"))
|
|
}
|
|
if defaultValue, ok := property["default"]; ok {
|
|
if text := strings.TrimSpace(fmt.Sprint(defaultValue)); text != "" {
|
|
qualifiers = append(qualifiers, "default: "+text)
|
|
}
|
|
}
|
|
detail += " (" + strings.Join(qualifiers, "; ") + ")"
|
|
if description := strings.TrimSpace(asSchemaString(property["description"])); description != "" {
|
|
detail += " - " + description
|
|
}
|
|
parts = append(parts, detail)
|
|
}
|
|
return strings.Join(parts, "; ")
|
|
}
|
|
|
|
func sortCommandInputs(names []string, required map[string]bool) {
|
|
sort.SliceStable(names, func(left int, right int) bool {
|
|
if required[names[left]] != required[names[right]] {
|
|
return required[names[left]]
|
|
}
|
|
return names[left] < names[right]
|
|
})
|
|
}
|
|
|
|
func schemaTypeLabel(property map[string]any) string {
|
|
switch strings.TrimSpace(asSchemaString(property["type"])) {
|
|
case "integer", "number":
|
|
return "number"
|
|
case "boolean":
|
|
return "true|false"
|
|
case "array", "object":
|
|
return "json"
|
|
default:
|
|
return strings.TrimSpace(asSchemaString(property["type"]))
|
|
}
|
|
}
|
|
|
|
func mapSchemaValue(value any) map[string]any {
|
|
if typed, ok := value.(map[string]any); ok {
|
|
return typed
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func asSchemaString(value any) string {
|
|
text, _ := value.(string)
|
|
return text
|
|
}
|
|
|
|
func schemaHasInput(schema map[string]any, name string) bool {
|
|
_, ok := mapSchemaValue(schema["properties"])[strings.TrimSpace(name)]
|
|
return ok
|
|
}
|
|
|
|
func stringSliceSchemaValue(value any) []string {
|
|
switch typed := value.(type) {
|
|
case []string:
|
|
return append([]string(nil), typed...)
|
|
case []any:
|
|
result := make([]string, 0, len(typed))
|
|
for _, item := range typed {
|
|
if text, ok := item.(string); ok {
|
|
result = append(result, text)
|
|
}
|
|
}
|
|
return result
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func normalizeCLICommandName(cliName string) string {
|
|
if cliName = strings.TrimSpace(cliName); cliName != "" {
|
|
return cliName
|
|
}
|
|
return "tutti"
|
|
}
|
|
|
|
func firstNonEmptyText(values ...string) string {
|
|
for _, value := range values {
|
|
if value = strings.TrimSpace(value); value != "" {
|
|
return value
|
|
}
|
|
}
|
|
return ""
|
|
}
|