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
398 lines
12 KiB
Go
398 lines
12 KiB
Go
package runtimeprep
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// CommandResolver is the immutable, agent-facing projection of one host
|
|
// command snapshot. It is shared by every Skill, policy section, and command
|
|
// guide rendered during one preparation.
|
|
type CommandResolver struct {
|
|
cliName string
|
|
commands []CommandCapability
|
|
byID map[string]CommandCapability
|
|
families map[string]struct{}
|
|
}
|
|
|
|
type commandArgument struct {
|
|
name string
|
|
value string
|
|
}
|
|
|
|
type commandArguments struct {
|
|
values []commandArgument
|
|
}
|
|
|
|
func newCommandResolver(cliName string, capabilities []CommandCapability) (*CommandResolver, error) {
|
|
resolver := &CommandResolver{
|
|
cliName: normalizeCLICommandName(cliName),
|
|
commands: make([]CommandCapability, 0, len(capabilities)),
|
|
byID: make(map[string]CommandCapability, len(capabilities)),
|
|
families: make(map[string]struct{}),
|
|
}
|
|
for _, capability := range capabilities {
|
|
if !commandVisibleToAgent(capability) {
|
|
continue
|
|
}
|
|
id := strings.TrimSpace(capability.ID)
|
|
if id == "" {
|
|
return nil, errors.New("agent command capability requires id")
|
|
}
|
|
if _, exists := resolver.byID[id]; exists {
|
|
return nil, fmt.Errorf("agent command capability id %q is duplicated", id)
|
|
}
|
|
capability.ID = id
|
|
capability.Path = normalizedCommandPath(capability.Path)
|
|
if len(capability.Path) == 0 {
|
|
return nil, fmt.Errorf("agent command capability %q requires path", id)
|
|
}
|
|
capability.InputSchema = cloneSchemaMap(capability.InputSchema)
|
|
if err := validateCommandInputSchema(id, capability.InputSchema); err != nil {
|
|
return nil, err
|
|
}
|
|
capability.InputSchema = executionFacingInputSchema(capability.ExecutionMode, capability.InputSchema)
|
|
capability.Output = cloneCommandCapabilityOutput(capability.Output)
|
|
resolver.commands = append(resolver.commands, capability)
|
|
resolver.byID[id] = capability
|
|
resolver.families[capability.Path[0]] = struct{}{}
|
|
}
|
|
return resolver, nil
|
|
}
|
|
|
|
func normalizedCommandPath(path []string) []string {
|
|
normalized := make([]string, 0, len(path))
|
|
for _, segment := range path {
|
|
if segment = strings.TrimSpace(segment); segment != "" {
|
|
normalized = append(normalized, segment)
|
|
}
|
|
}
|
|
return normalized
|
|
}
|
|
|
|
func validateCommandInputSchema(id string, schema map[string]any) error {
|
|
if schema == nil {
|
|
return nil
|
|
}
|
|
propertiesValue, hasProperties := schema["properties"]
|
|
properties := mapSchemaValue(propertiesValue)
|
|
if hasProperties && properties == nil {
|
|
return fmt.Errorf("agent command capability %q has invalid properties schema", id)
|
|
}
|
|
for name, value := range properties {
|
|
if strings.TrimSpace(name) == "" {
|
|
return fmt.Errorf("agent command capability %q has an empty input name", id)
|
|
}
|
|
property, ok := value.(map[string]any)
|
|
if !ok {
|
|
return fmt.Errorf("agent command capability %q input %q has invalid schema", id, name)
|
|
}
|
|
if enumValue, hasEnum := property["enum"]; hasEnum {
|
|
if _, ok := schemaStringSlice(enumValue); !ok {
|
|
return fmt.Errorf("agent command capability %q input %q has invalid enum", id, name)
|
|
}
|
|
}
|
|
}
|
|
required, ok := schemaStringSlice(schema["required"])
|
|
if _, exists := schema["required"]; exists && !ok {
|
|
return fmt.Errorf("agent command capability %q has invalid required inputs", id)
|
|
}
|
|
seen := make(map[string]struct{}, len(required))
|
|
for _, name := range required {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
return fmt.Errorf("agent command capability %q has an empty required input", id)
|
|
}
|
|
if _, exists := seen[name]; exists {
|
|
return fmt.Errorf("agent command capability %q repeats required input %q", id, name)
|
|
}
|
|
seen[name] = struct{}{}
|
|
if _, exists := properties[name]; !exists {
|
|
return fmt.Errorf("agent command capability %q requires unknown input %q", id, name)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func schemaStringSlice(value any) ([]string, bool) {
|
|
if value == nil {
|
|
return nil, true
|
|
}
|
|
switch typed := value.(type) {
|
|
case []string:
|
|
return append([]string(nil), typed...), true
|
|
case []any:
|
|
result := make([]string, 0, len(typed))
|
|
for _, item := range typed {
|
|
text, ok := item.(string)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
result = append(result, text)
|
|
}
|
|
return result, true
|
|
default:
|
|
return nil, false
|
|
}
|
|
}
|
|
|
|
func cloneCommandCapabilityOutput(output CommandCapabilityOutput) CommandCapabilityOutput {
|
|
if output.Table == nil {
|
|
return output
|
|
}
|
|
output.Table = &CommandTableOutput{
|
|
Columns: append([]CommandTableColumn(nil), output.Table.Columns...),
|
|
}
|
|
return output
|
|
}
|
|
|
|
func (r *CommandResolver) Has(id string) bool {
|
|
if r == nil {
|
|
return false
|
|
}
|
|
_, ok := r.byID[strings.TrimSpace(id)]
|
|
return ok
|
|
}
|
|
|
|
func (r *CommandResolver) HasAll(ids ...string) bool {
|
|
if len(ids) == 0 {
|
|
return false
|
|
}
|
|
for _, id := range ids {
|
|
if !r.Has(id) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (r *CommandResolver) HasFamily(family string) bool {
|
|
if r == nil {
|
|
return false
|
|
}
|
|
_, ok := r.families[strings.TrimSpace(family)]
|
|
return ok
|
|
}
|
|
|
|
func (r *CommandResolver) HasInput(id string, name string) bool {
|
|
capability, ok := r.capability(id)
|
|
return ok && schemaHasInput(capability.InputSchema, strings.TrimSpace(name))
|
|
}
|
|
|
|
func (r *CommandResolver) InputValues(id string, name string) []string {
|
|
capability, ok := r.capability(id)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
property := mapSchemaValue(mapSchemaValue(capability.InputSchema["properties"])[strings.TrimSpace(name)])
|
|
return stringSliceSchemaValue(property["enum"])
|
|
}
|
|
|
|
func (r *CommandResolver) Path(id string) (string, error) {
|
|
capability, ok := r.capability(id)
|
|
if !ok {
|
|
return "", fmt.Errorf("agent command %q is not advertised", strings.TrimSpace(id))
|
|
}
|
|
return commandPath(capability.Path), nil
|
|
}
|
|
|
|
func (r *CommandResolver) Command(id string, argumentSets ...commandArguments) (string, error) {
|
|
capability, ok := r.capability(id)
|
|
if !ok {
|
|
return "", fmt.Errorf("agent command %q is not advertised", strings.TrimSpace(id))
|
|
}
|
|
arguments := mergeCommandArguments(argumentSets)
|
|
properties := mapSchemaValue(capability.InputSchema["properties"])
|
|
values := make(map[string]string, len(arguments))
|
|
argumentOrder := make([]string, 0, len(arguments))
|
|
for _, argument := range arguments {
|
|
property, exists := properties[argument.name]
|
|
if !exists {
|
|
return "", fmt.Errorf("agent command %q does not accept --%s", capability.ID, argument.name)
|
|
}
|
|
if err := validateCommandArgumentValue(capability.ID, argument, mapSchemaValue(property)); err != nil {
|
|
return "", err
|
|
}
|
|
if _, exists := values[argument.name]; !exists {
|
|
argumentOrder = append(argumentOrder, argument.name)
|
|
}
|
|
values[argument.name] = argument.value
|
|
}
|
|
|
|
var command strings.Builder
|
|
command.WriteString(r.cliName)
|
|
command.WriteByte(' ')
|
|
command.WriteString(commandPath(capability.Path))
|
|
|
|
emitted := make(map[string]struct{}, len(values))
|
|
for _, name := range stringSliceSchemaValue(capability.InputSchema["required"]) {
|
|
name = strings.TrimSpace(name)
|
|
value := values[name]
|
|
if strings.TrimSpace(value) == "" {
|
|
value = "<" + name + ">"
|
|
}
|
|
appendCommandFlag(&command, name, value, mapSchemaValue(properties[name]))
|
|
emitted[name] = struct{}{}
|
|
}
|
|
for _, name := range argumentOrder {
|
|
if _, exists := emitted[name]; exists {
|
|
continue
|
|
}
|
|
appendCommandFlag(&command, name, values[name], mapSchemaValue(properties[name]))
|
|
}
|
|
if commandSupportsJSON(capability.Output) {
|
|
command.WriteString(" --json")
|
|
}
|
|
return command.String(), nil
|
|
}
|
|
|
|
func validateCommandArgumentValue(id string, argument commandArgument, property map[string]any) error {
|
|
value := strings.TrimSpace(argument.value)
|
|
if strings.HasPrefix(value, "<") && strings.HasSuffix(value, ">") {
|
|
return nil
|
|
}
|
|
if enum := stringSliceSchemaValue(property["enum"]); len(enum) > 0 {
|
|
for _, allowed := range enum {
|
|
if value == allowed {
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf(
|
|
"agent command %q input --%s does not accept %q",
|
|
id,
|
|
argument.name,
|
|
argument.value,
|
|
)
|
|
}
|
|
if schemaTypeLabel(property) == "true|false" &&
|
|
!strings.EqualFold(value, "true") &&
|
|
!strings.EqualFold(value, "false") {
|
|
return fmt.Errorf("agent command %q input --%s requires true or false", id, argument.name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func mergeCommandArguments(sets []commandArguments) []commandArgument {
|
|
var result []commandArgument
|
|
for _, set := range sets {
|
|
result = append(result, set.values...)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func appendCommandFlag(command *strings.Builder, name string, value string, property map[string]any) {
|
|
command.WriteString(" --")
|
|
command.WriteString(name)
|
|
if schemaTypeLabel(property) == "true|false" && strings.EqualFold(strings.TrimSpace(value), "true") {
|
|
return
|
|
}
|
|
command.WriteByte(' ')
|
|
command.WriteString(shellExampleValue(value))
|
|
}
|
|
|
|
func shellExampleValue(value string) string {
|
|
if strings.ContainsAny(value, " \t") && !strings.HasPrefix(value, "'") && !strings.HasPrefix(value, "\"") {
|
|
return "\"" + value + "\""
|
|
}
|
|
return value
|
|
}
|
|
|
|
func commandSupportsJSON(output CommandCapabilityOutput) bool {
|
|
return output.JSON || strings.TrimSpace(output.DefaultMode) == "json"
|
|
}
|
|
|
|
func commandTemplateArguments(values ...any) (commandArguments, error) {
|
|
if len(values)%2 != 0 {
|
|
return commandArguments{}, errors.New("args requires name/value pairs")
|
|
}
|
|
result := commandArguments{values: make([]commandArgument, 0, len(values)/2)}
|
|
for index := 0; index < len(values); index += 2 {
|
|
name := strings.TrimSpace(fmt.Sprint(values[index]))
|
|
if name == "" {
|
|
return commandArguments{}, errors.New("args requires a non-empty input name")
|
|
}
|
|
result.values = append(result.values, commandArgument{
|
|
name: name,
|
|
value: fmt.Sprint(values[index+1]),
|
|
})
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (r *CommandResolver) capability(id string) (CommandCapability, bool) {
|
|
if r == nil {
|
|
return CommandCapability{}, false
|
|
}
|
|
capability, ok := r.byID[strings.TrimSpace(id)]
|
|
return capability, ok
|
|
}
|
|
|
|
func (r *CommandResolver) Guide() (string, error) {
|
|
if r == nil || len(r.commands) == 0 {
|
|
return "- No agent-facing runtime CLI commands were advertised by the current host.", nil
|
|
}
|
|
lines := make([]string, 0, len(r.commands))
|
|
for _, capability := range r.commands {
|
|
example, err := r.Command(capability.ID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
line := "- " + firstNonEmptyText(capability.Summary, capability.ID) + ": `" + example + "`"
|
|
if description := commandGuideDescription(capability); description != "" {
|
|
line += " - " + description
|
|
}
|
|
if details := inputDetailsForCommand(capability.ID, capability.InputSchema); details != "" {
|
|
line += " Arguments: " + details
|
|
}
|
|
lines = append(lines, line)
|
|
}
|
|
return strings.Join(lines, "\n"), nil
|
|
}
|
|
|
|
func (r *CommandResolver) Families() []string {
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
values := make([]string, 0, len(r.families))
|
|
for family := range r.families {
|
|
values = append(values, family)
|
|
}
|
|
sort.Strings(values)
|
|
return values
|
|
}
|
|
|
|
func commandGuideDescription(capability CommandCapability) string {
|
|
parts := make([]string, 0, 3)
|
|
if description := strings.TrimSpace(capability.Description); description != "" {
|
|
parts = append(parts, description)
|
|
}
|
|
if capability.Source.Kind == CommandSourceApp {
|
|
if appName := strings.TrimSpace(capability.Source.AppName); appName != "" {
|
|
parts = append(parts, "Provided by workspace app "+appName+".")
|
|
}
|
|
if appID := strings.TrimSpace(capability.Source.AppID); appID != "" {
|
|
parts = append(parts, "App id: "+appID+".")
|
|
}
|
|
}
|
|
return strings.Join(parts, " ")
|
|
}
|
|
|
|
func (r *CommandResolver) OutputModes() []string {
|
|
modes := make(map[string]struct{})
|
|
if r != nil {
|
|
for _, capability := range r.commands {
|
|
if mode := strings.TrimSpace(capability.Output.DefaultMode); mode != "" {
|
|
modes[mode] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
values := make([]string, 0, len(modes))
|
|
for mode := range modes {
|
|
values = append(values, mode)
|
|
}
|
|
sort.Strings(values)
|
|
return values
|
|
}
|