Merge remote-tracking branch 'origin/main' into codex/fix-plugin-command-registration
# Conflicts: # CHANGELOG.md
This commit is contained in:
@@ -8,6 +8,7 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and th
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Schema CLI path compatibility** — user-facing Schema lookups once again accept space-, dot-, and slash-separated CLI paths without weakening strict canonical identity resolution.
|
||||
- **Plugin CLI overlays** — installed plugins register their manifest-authored command trees again for HTTP and stdio servers, and a plugin may now replace a hidden compatibility fallback (for example `conference`) instead of being skipped as a distribution conflict.
|
||||
|
||||
## [1.0.53] - 2026-07-21
|
||||
|
||||
@@ -304,3 +304,14 @@ func splitSchemaPathTokens(raw string) []string {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// normalizeSchemaQueryCLIPath accepts the historical query spellings while
|
||||
// keeping authored Registry CLI paths strict and space-separated. Canonical
|
||||
// identity lookup still runs before this compatibility normalization.
|
||||
func normalizeSchemaQueryCLIPath(path string) string {
|
||||
parts := splitSchemaPathTokens(strings.TrimSpace(path))
|
||||
if len(parts) > 0 && parts[0] == "dws" {
|
||||
parts = parts[1:]
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
@@ -290,7 +290,7 @@ func schemaPayloadFromLoadedCatalog(loaded loadedSchemaCatalog, args []string) (
|
||||
return payload, nil
|
||||
}
|
||||
raw := strings.TrimSpace(args[0])
|
||||
if tool, ok := loaded.Index.Resolve(raw); ok {
|
||||
if tool, ok := loaded.Index.ResolveQuery(raw); ok {
|
||||
return schemaToolForResolvedPath(tool, raw).ToPayload()
|
||||
}
|
||||
tokens := splitSchemaPathTokens(raw)
|
||||
|
||||
@@ -194,6 +194,23 @@ func TestCrossPlatformCoverageSchemaCatalogLookupAndConversionEdges(t *testing.T
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmbeddedSchemaLookupAcceptsCompatibleCLIPathSeparators(t *testing.T) {
|
||||
loaded := embeddedSchemaCatalog()
|
||||
for _, path := range []string{
|
||||
"dev app list",
|
||||
"dev.app.list",
|
||||
"dev/app/list",
|
||||
} {
|
||||
payload, err := schemaPayloadFromLoadedCatalog(loaded, []string{path})
|
||||
if err != nil {
|
||||
t.Fatalf("schemaPayloadFromLoadedCatalog(%q) error = %v", path, err)
|
||||
}
|
||||
if got := schemaString(payload["canonical_path"]); got != "dev.list_dev_app" {
|
||||
t.Fatalf("schemaPayloadFromLoadedCatalog(%q) canonical_path = %q, want %q", path, got, "dev.list_dev_app")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type catalogHookSnapshot struct {
|
||||
parameterBindings func(BoundCommandRegistry, SchemaRegistry) error
|
||||
dryRun func(SchemaRegistry) error
|
||||
|
||||
@@ -617,6 +617,22 @@ func (i SchemaIndex) Resolve(path string) (ToolSpec, bool) {
|
||||
return i.registry.Products[location.product].Tools[location.tool], true
|
||||
}
|
||||
|
||||
// ResolveQuery adds compatibility for dotted and slash-separated CLI paths at
|
||||
// the user-facing query boundary. Resolve remains strict because Registry
|
||||
// validation uses it to detect missing canonical identities without falling
|
||||
// through to a similarly spelled CLI path.
|
||||
func (i SchemaIndex) ResolveQuery(path string) (ToolSpec, bool) {
|
||||
if tool, ok := i.Resolve(path); ok {
|
||||
return tool, true
|
||||
}
|
||||
canonical, ok := i.byCLIPath[normalizeSchemaQueryCLIPath(path)]
|
||||
if !ok {
|
||||
return ToolSpec{}, false
|
||||
}
|
||||
location := i.byCanonical[canonical]
|
||||
return i.registry.Products[location.product].Tools[location.tool], true
|
||||
}
|
||||
|
||||
// CanonicalPaths returns the complete tool identity set in stable order.
|
||||
func (i SchemaIndex) CanonicalPaths() []string {
|
||||
paths := make([]string, 0, len(i.byCanonical))
|
||||
|
||||
@@ -514,6 +514,19 @@ func TestSchemaRegistryIndexResolvesCanonicalCLIAndAlias(t *testing.T) {
|
||||
t.Fatalf("Resolve(%q) = %#v, %v", path, resolved.Identity, ok)
|
||||
}
|
||||
}
|
||||
for _, path := range []string{
|
||||
"calendar.attendee.delete",
|
||||
"calendar/attendee/delete",
|
||||
"dws.calendar.attendee.delete",
|
||||
} {
|
||||
resolved, ok := index.ResolveQuery(path)
|
||||
if !ok || resolved.Identity.CanonicalPath != "calendar.attendee_delete" {
|
||||
t.Fatalf("ResolveQuery(%q) = %#v, %v", path, resolved.Identity, ok)
|
||||
}
|
||||
}
|
||||
if _, ok := index.ResolveQuery("calendar.attendee.unknown"); ok {
|
||||
t.Fatal("unknown dotted CLI path unexpectedly resolved")
|
||||
}
|
||||
if got := index.CanonicalPaths(); !reflect.DeepEqual(got, []string{"calendar.attendee_delete"}) {
|
||||
t.Fatalf("CanonicalPaths() = %#v", got)
|
||||
}
|
||||
|
||||
@@ -154,6 +154,35 @@ func TestSelectionExplicitEmptyListSurvivesFinalDelivery(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompatibleSchemaAliasSeparatorsSurviveFinalDelivery(t *testing.T) {
|
||||
snapshot := schemaDeliveryTestSnapshot(schemaDeliveryTestTool{
|
||||
Canonical: "sample.run",
|
||||
CLIPath: "sample category run",
|
||||
Aliases: []string{"sample legacy execute"},
|
||||
})
|
||||
encoded, err := json.Marshal(snapshot)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
loaded, err := decodeSchemaCatalogSnapshot(encoded)
|
||||
if err != nil {
|
||||
t.Fatalf("decodeSchemaCatalogSnapshot(): %v", err)
|
||||
}
|
||||
canonical, err := schemaPayloadFromLoadedCatalog(loaded, []string{"sample.run"})
|
||||
if err != nil {
|
||||
t.Fatalf("canonical query: %v", err)
|
||||
}
|
||||
for _, path := range []string{"sample legacy execute", "sample.legacy.execute", "sample/legacy/execute"} {
|
||||
alias, aliasErr := schemaPayloadFromLoadedCatalog(loaded, []string{path})
|
||||
if aliasErr != nil {
|
||||
t.Fatalf("alias query %q: %v", path, aliasErr)
|
||||
}
|
||||
if problem := schemaAliasViewProblem(canonical, alias, "sample legacy execute"); problem != "" {
|
||||
t.Fatalf("alias projection for %q: %s", path, problem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSchemaDeliveryInvariantsAllowsOnlyEnvelopeHashes(t *testing.T) {
|
||||
snapshot := schemaDeliveryTestSnapshot(schemaDeliveryTestTool{Canonical: "sample.run", CLIPath: "sample run"})
|
||||
snapshot.SurfaceHash = "sha256:reviewed-command-registry"
|
||||
|
||||
@@ -284,7 +284,7 @@ func runtimeSchemaPayloadFromRegistry(registry SchemaRegistry, args []string) (m
|
||||
}
|
||||
|
||||
raw := strings.TrimSpace(args[0])
|
||||
if tool, ok := index.Resolve(raw); ok {
|
||||
if tool, ok := index.ResolveQuery(raw); ok {
|
||||
tool = schemaToolForResolvedPath(tool, raw)
|
||||
return renderRegistryToolPayload(tool)
|
||||
}
|
||||
@@ -338,7 +338,7 @@ func runtimeSchemaAllPayloadFromRegistry(registry SchemaRegistry) (map[string]an
|
||||
}
|
||||
|
||||
func schemaToolForResolvedPath(tool ToolSpec, raw string) ToolSpec {
|
||||
normalized := normalizeSchemaCLIPath(raw)
|
||||
normalized := normalizeSchemaQueryCLIPath(raw)
|
||||
if normalized == "" || normalized == tool.Identity.CLIPath || normalized == tool.Identity.PrimaryCLIPath {
|
||||
return tool
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user