bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
234 lines
7.0 KiB
Go
234 lines
7.0 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package manifest
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
type Source string
|
|
|
|
const (
|
|
SourceBuiltin Source = "builtin"
|
|
SourceShortcut Source = "shortcut"
|
|
SourceService Source = "service"
|
|
)
|
|
|
|
type Manifest struct {
|
|
SchemaVersion int `json:"schema_version"`
|
|
Commands []Command `json:"commands"`
|
|
}
|
|
|
|
type Command struct {
|
|
Path string `json:"path"`
|
|
CanonicalPath string `json:"canonical_path,omitempty"`
|
|
Domain string `json:"domain,omitempty"`
|
|
Use string `json:"use"`
|
|
Short string `json:"short,omitempty"`
|
|
Example string `json:"example,omitempty"`
|
|
Hidden bool `json:"hidden,omitempty"`
|
|
Runnable bool `json:"runnable"`
|
|
Source Source `json:"source"`
|
|
Generated bool `json:"generated,omitempty"`
|
|
Risk string `json:"risk,omitempty"`
|
|
Identities []string `json:"identities,omitempty"`
|
|
Flags []Flag `json:"flags,omitempty"`
|
|
DefaultFields []string `json:"default_fields,omitempty"`
|
|
}
|
|
|
|
type Flag struct {
|
|
Name string `json:"name"`
|
|
Shorthand string `json:"shorthand,omitempty"`
|
|
Usage string `json:"usage,omitempty"`
|
|
Hidden bool `json:"hidden,omitempty"`
|
|
Required bool `json:"required,omitempty"`
|
|
TakesValue bool `json:"takes_value"`
|
|
DefValue string `json:"default,omitempty"`
|
|
NoOptValue string `json:"no_opt_value,omitempty"`
|
|
Annotations map[string][]string `json:"annotations,omitempty"`
|
|
}
|
|
|
|
const (
|
|
KindCommandManifest = "command-manifest"
|
|
KindCommandIndex = "command-index"
|
|
|
|
MaxManifestBytes = 16 * 1024 * 1024
|
|
MaxCommandsPerManifest = 10000
|
|
MaxFlagsPerCommand = 200
|
|
MaxManifestStringBytes = 8192
|
|
MaxAnnotationValues = 100
|
|
MaxAnnotationValueBytes = 8192
|
|
)
|
|
|
|
func (m Manifest) Validate(kind string) error {
|
|
if kind != KindCommandManifest && kind != KindCommandIndex {
|
|
return fmt.Errorf("unknown manifest kind %q", kind)
|
|
}
|
|
if m.SchemaVersion != 1 {
|
|
return fmt.Errorf("%s schema_version must be 1", kind)
|
|
}
|
|
if len(m.Commands) == 0 {
|
|
return fmt.Errorf("%s must contain at least one command", kind)
|
|
}
|
|
if len(m.Commands) > MaxCommandsPerManifest {
|
|
return fmt.Errorf("%s has too many commands: %d", kind, len(m.Commands))
|
|
}
|
|
|
|
seenCommands := make(map[string]struct{}, len(m.Commands))
|
|
hasService := false
|
|
for i, cmd := range m.Commands {
|
|
if err := validateCommand(kind, i, cmd); err != nil {
|
|
return err
|
|
}
|
|
if _, ok := seenCommands[cmd.Path]; ok {
|
|
return fmt.Errorf("%s command path is duplicated: %s", kind, cmd.Path)
|
|
}
|
|
seenCommands[cmd.Path] = struct{}{}
|
|
if cmd.Source == SourceService {
|
|
hasService = true
|
|
}
|
|
}
|
|
|
|
switch kind {
|
|
case KindCommandManifest:
|
|
if hasService {
|
|
return fmt.Errorf("%s must not contain generated service commands", kind)
|
|
}
|
|
case KindCommandIndex:
|
|
if !hasService {
|
|
return fmt.Errorf("%s must contain service commands", kind)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateCommand(kind string, i int, cmd Command) error {
|
|
prefix := fmt.Sprintf("%s commands[%d]", kind, i)
|
|
if err := validateString(prefix+".path", cmd.Path, true); err != nil {
|
|
return err
|
|
}
|
|
if err := validateString(prefix+".canonical_path", cmd.CanonicalPath, false); err != nil {
|
|
return err
|
|
}
|
|
if cmd.CanonicalPath != "" && cmd.CanonicalPath != CanonicalCommandPath(cmd.Path) {
|
|
return fmt.Errorf("%s.canonical_path = %q, want %q", prefix, cmd.CanonicalPath, CanonicalCommandPath(cmd.Path))
|
|
}
|
|
if err := validateString(prefix+".domain", cmd.Domain, false); err != nil {
|
|
return err
|
|
}
|
|
if err := validateString(prefix+".use", cmd.Use, false); err != nil {
|
|
return err
|
|
}
|
|
if err := validateString(prefix+".short", cmd.Short, false); err != nil {
|
|
return err
|
|
}
|
|
if err := validateString(prefix+".example", cmd.Example, false); err != nil {
|
|
return err
|
|
}
|
|
if err := validateString(prefix+".risk", cmd.Risk, false); err != nil {
|
|
return err
|
|
}
|
|
switch cmd.Source {
|
|
case SourceBuiltin, SourceShortcut, SourceService:
|
|
default:
|
|
return fmt.Errorf("%s.source is invalid: %q", prefix, cmd.Source)
|
|
}
|
|
if cmd.Source == SourceService && !cmd.Generated {
|
|
return fmt.Errorf("%s.generated must be true for service commands", prefix)
|
|
}
|
|
if cmd.Generated && cmd.Source != SourceService {
|
|
return fmt.Errorf("%s.generated can only be true for service commands", prefix)
|
|
}
|
|
if len(cmd.Flags) > MaxFlagsPerCommand {
|
|
return fmt.Errorf("%s has too many flags: %d", prefix, len(cmd.Flags))
|
|
}
|
|
for j, identity := range cmd.Identities {
|
|
if err := validateString(fmt.Sprintf("%s.identities[%d]", prefix, j), identity, true); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for j, field := range cmd.DefaultFields {
|
|
if err := validateString(fmt.Sprintf("%s.default_fields[%d]", prefix, j), field, true); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
seenFlags := make(map[string]struct{}, len(cmd.Flags))
|
|
for j, flag := range cmd.Flags {
|
|
if err := validateFlag(prefix, j, flag); err != nil {
|
|
return err
|
|
}
|
|
if _, ok := seenFlags[flag.Name]; ok {
|
|
return fmt.Errorf("%s flags[%d].name is duplicated: %s", prefix, j, flag.Name)
|
|
}
|
|
seenFlags[flag.Name] = struct{}{}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateFlag(commandPrefix string, i int, flag Flag) error {
|
|
prefix := fmt.Sprintf("%s.flags[%d]", commandPrefix, i)
|
|
if err := validateString(prefix+".name", flag.Name, true); err != nil {
|
|
return err
|
|
}
|
|
if strings.ContainsAny(flag.Name, " \t\r\n") {
|
|
return fmt.Errorf("%s.name must not contain whitespace", prefix)
|
|
}
|
|
for _, item := range []struct {
|
|
name string
|
|
value string
|
|
}{
|
|
{name: "shorthand", value: flag.Shorthand},
|
|
{name: "usage", value: flag.Usage},
|
|
{name: "default", value: flag.DefValue},
|
|
{name: "no_opt_value", value: flag.NoOptValue},
|
|
} {
|
|
if err := validateString(prefix+"."+item.name, item.value, false); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for key, values := range flag.Annotations {
|
|
if err := validateString(prefix+".annotations key", key, true); err != nil {
|
|
return err
|
|
}
|
|
if len(values) > MaxAnnotationValues {
|
|
return fmt.Errorf("%s.annotations[%q] has too many values: %d", prefix, key, len(values))
|
|
}
|
|
for j, value := range values {
|
|
if value == "" {
|
|
return fmt.Errorf("%s.annotations[%q][%d] must not be empty", prefix, key, j)
|
|
}
|
|
if len(value) > MaxAnnotationValueBytes {
|
|
return fmt.Errorf("%s.annotations[%q][%d] is too large", prefix, key, j)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateString(label, value string, required bool) error {
|
|
if required && value == "" {
|
|
return fmt.Errorf("%s must not be empty", label)
|
|
}
|
|
if len(value) > MaxManifestStringBytes {
|
|
return fmt.Errorf("%s is too large", label)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func CanonicalCommandPath(path string) string {
|
|
parts := strings.Fields(path)
|
|
for i, part := range parts {
|
|
prefix := ""
|
|
if strings.HasPrefix(part, "+") {
|
|
prefix = "+"
|
|
part = strings.TrimPrefix(part, "+")
|
|
}
|
|
part = strings.ReplaceAll(part, ".", "-")
|
|
part = strings.ReplaceAll(part, "_", "-")
|
|
parts[i] = prefix + part
|
|
}
|
|
return strings.Join(parts, " ")
|
|
}
|