Files
dingtalk-workspace-cli/internal/cli/schema_catalog.go
T
玉澜 4b0f71eedc test: close changed-code coverage gaps to satisfy the coverage gate
- fetch_mcp_metadata: extract run()/resolveToken()/writeMetadata with
  injectable deps (keychain, servers, lister, registry, exit); full-path
  tests reach 100% file coverage.
- internal/cli: drop dead initSafetyByCLIPath (superseded by ResolveMeta),
  split buildMetaByCLIPath / assembleSchemaCatalogSnapshot /
  assembleCommandRegistryFrom / mergedCommandRegistryJSON so shard and
  malformed-snapshot failure modes are testable; cover catalog structure
  violation formatting (sort/truncate) and RenderSafetyAnnotation.
- generators: cover registry shard merge and catalog shard write failure
  modes.
- helpers/cmdutil: cover LeafSpec default/server dispatch, transform error
  propagation, default env hint, devapp member remove validate chain, and
  the required-flags error helpers.

Local gate: overall 90.17% vs merge-base 89.96%, changed-code 100%
(861 statements); make policy and go test ./... green.
2026-07-28 18:05:20 +08:00

495 lines
18 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"crypto/sha256"
"embed"
"encoding/hex"
"encoding/json"
"fmt"
"io/fs"
"strings"
"sync"
"sync/atomic"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/spf13/cobra"
)
const SchemaCatalogSnapshotVersion = 1
// The release Catalog is committed as a per-product split so concurrent
// feature PRs only rewrite the shard for the product they touch:
//
// schema_catalog/catalog.json global envelope + Catalog map
// schema_catalog/tools/<product>.json that product's leaf ToolSpecs
//
// The loader reassembles the exact same SchemaCatalogSnapshot, so the
// source_hash integrity check is identical to the single-file layout.
//
//go:embed schema_catalog/catalog.json
var embeddedSchemaCatalogEnvelopeJSON []byte
//go:embed schema_catalog/tools
var embeddedSchemaCatalogTools embed.FS
// SchemaCatalogSnapshot is the release-stable Agent contract. Catalog holds
// the progressive product/tool index; Tools holds full leaf parameter schemas.
// It intentionally contains no endpoint, credential, or runtime cache data.
type SchemaCatalogSnapshot struct {
Version int `json:"version"`
SourceHash string `json:"source_hash"`
SurfaceHash string `json:"surface_hash,omitempty"`
Catalog map[string]any `json:"catalog"`
Tools map[string]map[string]any `json:"tools"`
}
// SchemaCatalogBuildOptions carries release-envelope inputs which are checked
// against the effective reviewed CommandRegistry. The command set is not an
// option: visibility is resolved by EffectiveCommandRegistry before assembly,
// and every public command must be delivered.
type SchemaCatalogBuildOptions struct {
RegistryHash string
}
type loadedSchemaCatalog struct {
Snapshot SchemaCatalogSnapshot
Registry SchemaRegistry
Index SchemaIndex
}
var (
runtimeEmbeddedSchemaCatalogOnce sync.Once
runtimeEmbeddedSchemaCatalog loadedSchemaCatalog
runtimeEmbeddedSchemaCatalogErr error
)
var runtimeEmbeddedSchemaCatalogLazyLoadCount atomic.Uint64
func embeddedSchemaCatalog() loadedSchemaCatalog {
runtimeEmbeddedSchemaCatalogOnce.Do(func() {
runtimeEmbeddedSchemaCatalogLazyLoadCount.Add(1)
runtimeEmbeddedSchemaCatalog, runtimeEmbeddedSchemaCatalogErr = assembleEmbeddedSchemaCatalog()
})
return runtimeEmbeddedSchemaCatalog
}
// schemaCatalogEnvelope is the global half of the split release Catalog. It
// mirrors the generator's envelope struct; the Catalog map and release hashes
// do not partition by product and stay in one file.
type schemaCatalogEnvelope struct {
Version int `json:"version"`
SurfaceHash string `json:"surface_hash,omitempty"`
SourceHash string `json:"source_hash"`
Catalog map[string]any `json:"catalog"`
}
// schemaCatalogToolShard mirrors the per-product shard written by the
// generator. Only the product and its leaf ToolSpecs are stored here.
type schemaCatalogToolShard struct {
Product string `json:"product"`
Tools map[string]map[string]any `json:"tools"`
}
// assembleEmbeddedSchemaCatalog reassembles the split release Catalog shards
// into the same SchemaCatalogSnapshot the single-file layout produced, then
// validates it through the production loader. source_hash still covers the
// whole payload: if any shard is missing, stale, or tampered, the content hash
// check in loadSchemaCatalogSnapshot fails exactly as before.
func assembleEmbeddedSchemaCatalog() (loadedSchemaCatalog, error) {
snapshot, err := assembleSchemaCatalogSnapshot(embeddedSchemaCatalogEnvelopeJSON, embeddedSchemaCatalogTools, "schema_catalog/tools")
if err != nil {
return loadedSchemaCatalog{}, err
}
return loadSchemaCatalogSnapshot(snapshot)
}
// assembleSchemaCatalogSnapshot merges an envelope document and a directory of
// per-product tool shards back into the single-document snapshot shape. Split
// from assembleEmbeddedSchemaCatalog so shard failure modes stay testable
// against fake filesystems.
func assembleSchemaCatalogSnapshot(envelopeJSON []byte, shards fs.FS, dir string) (SchemaCatalogSnapshot, error) {
var envelope schemaCatalogEnvelope
if err := decodeStrictSchemaJSON(envelopeJSON, &envelope); err != nil {
return SchemaCatalogSnapshot{}, fmt.Errorf("decode embedded schema catalog.json: %w", err)
}
entries, err := fs.ReadDir(shards, dir)
if err != nil {
return SchemaCatalogSnapshot{}, fmt.Errorf("read embedded schema catalog tools directory: %w", err)
}
tools := make(map[string]map[string]any, len(entries)*8)
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
continue
}
data, readErr := fs.ReadFile(shards, dir+"/"+entry.Name())
if readErr != nil {
return SchemaCatalogSnapshot{}, fmt.Errorf("read embedded schema catalog shard %s: %w", entry.Name(), readErr)
}
var shard schemaCatalogToolShard
if err := decodeStrictSchemaJSON(data, &shard); err != nil {
return SchemaCatalogSnapshot{}, fmt.Errorf("decode embedded schema catalog shard %s: %w", entry.Name(), err)
}
for canonical, spec := range shard.Tools {
tools[canonical] = spec
}
}
return SchemaCatalogSnapshot{
Version: envelope.Version,
SurfaceHash: envelope.SurfaceHash,
SourceHash: envelope.SourceHash,
Catalog: envelope.Catalog,
Tools: tools,
}, nil
}
func embeddedSchemaCatalogError() error {
_ = embeddedSchemaCatalog()
return runtimeEmbeddedSchemaCatalogErr
}
var (
buildCatalogValidateParameterBindings = ValidateSchemaParameterBindingDelivery
buildCatalogValidateDryRun = ValidateReviewedDryRunCapabilityDelivery
buildCatalogValidateExamples = ValidateEmbeddedManualAgentExampleDelivery
buildCatalogValidateCompleteness = validateResolvedRuntimeSchemaCompleteness
buildCatalogValidateRegistry = validateSchemaRegistryAgainstCommandRegistry
buildCatalogValidateInterfaces = validateSchemaRegistryInterfaces
buildCatalogValidateAgentMetadata = validateSchemaRegistryAgentMetadata
buildCatalogValidateProvenance = validateFinalSchemaProvenanceCoverage
buildCatalogValidateDelivery = ValidateSchemaDeliveryInvariants
buildCatalogValidateFinalCompleteness = validateResolvedSchemaCatalogDeliveryCompleteness
loadCatalogValidateInterfaces = validateSchemaRegistryInterfaces
loadCatalogValidateProvenance = validateFinalSchemaProvenanceCoverage
loadCatalogValidateAgentMetadata = validateSchemaRegistryAgentMetadata
renderEmbeddedSchemaAll = func(registry SchemaRegistry) (map[string]any, error) { return registry.ToPayload() }
renderEmbeddedSchemaOverview = func(registry SchemaRegistry) (map[string]any, error) { return registry.ToOverviewPayload() }
renderSchemaProductSummary = func(product ProductSpec) (map[string]any, error) { return product.ToSummaryPayload() }
renderSchemaToolSummary = func(tool ToolSpec) (map[string]any, error) { return tool.ToSummaryPayload() }
)
// BuildSchemaCatalogSnapshot renders a deterministic Catalog from one
// resolved source-to-delivery hand-off. It deliberately accepts no Cobra root:
// reapplying manual hints or rebuilding SchemaRegistry at this boundary would
// allow generation gates to validate one candidate while publishing another.
func BuildSchemaCatalogSnapshot(resolved ResolvedSchemaBuild, options SchemaCatalogBuildOptions) (SchemaCatalogSnapshot, error) {
if resolved.root == nil {
return SchemaCatalogSnapshot{}, fmt.Errorf("schema Catalog source was not created by ResolveSchemaBuild")
}
registry := resolved.registry
effectiveCommands := resolved.effective
registryHash := strings.TrimSpace(options.RegistryHash)
if registryHash == "" {
registryHash = effectiveCommands.SourceHash()
} else if registryHash != effectiveCommands.SourceHash() {
return SchemaCatalogSnapshot{}, fmt.Errorf("provided Registry hash %q disagrees with effective CommandRegistry %q", registryHash, effectiveCommands.SourceHash())
}
if err := buildCatalogValidateParameterBindings(resolved.bound, registry); err != nil {
return SchemaCatalogSnapshot{}, fmt.Errorf("validate final Schema parameter binding delivery: %w", err)
}
if err := buildCatalogValidateDryRun(registry); err != nil {
return SchemaCatalogSnapshot{}, fmt.Errorf("validate reviewed dry-run capability delivery: %w", err)
}
if _, err := buildCatalogValidateExamples(resolved.bound, registry); err != nil {
return SchemaCatalogSnapshot{}, fmt.Errorf("validate final Manual Agent example delivery: %w", err)
}
if err := buildCatalogValidateCompleteness(resolved.root, resolved.bound); err != nil {
return SchemaCatalogSnapshot{}, fmt.Errorf("validate reverse command-tree completeness: %w", err)
}
if err := buildCatalogValidateRegistry(registry, effectiveCommands); err != nil {
return SchemaCatalogSnapshot{}, fmt.Errorf("validate typed Schema registry against reviewed CommandRegistry: %w", err)
}
if err := buildCatalogValidateInterfaces(registry); err != nil {
return SchemaCatalogSnapshot{}, fmt.Errorf("validate final Schema interface disposition: %w", err)
}
if err := buildCatalogValidateAgentMetadata(registry); err != nil {
return SchemaCatalogSnapshot{}, fmt.Errorf("validate final Schema Agent metadata set: %w", err)
}
if err := buildCatalogValidateProvenance(registry); err != nil {
return SchemaCatalogSnapshot{}, fmt.Errorf("validate final Schema provenance: %w", err)
}
// Visibility has already selected the complete public set in
// collectRuntimeSchemaEntriesFromBound. Do not apply a post-assembly
// allowlist: doing so could silently erase an otherwise valid reviewed
// manual-only command after the exact-set validation above has passed.
registry.Source = "embedded-command-catalog"
payload, err := registry.ToSnapshotPayload()
if err != nil {
return SchemaCatalogSnapshot{}, fmt.Errorf("serialize typed Schema registry: %w", err)
}
snapshot := SchemaCatalogSnapshot{
Version: SchemaCatalogSnapshotVersion,
SurfaceHash: registryHash,
Catalog: payload.Catalog,
Tools: payload.Tools,
}
snapshot.SourceHash = schemaCatalogSnapshotHash(snapshot)
if err := buildCatalogValidateDelivery(registry, snapshot); err != nil {
return SchemaCatalogSnapshot{}, fmt.Errorf("validate final Schema delivery invariants: %w", err)
}
if err := buildCatalogValidateFinalCompleteness(resolved.root, resolved.bound, snapshot); err != nil {
return SchemaCatalogSnapshot{}, fmt.Errorf("validate final Catalog delivery completeness: %w", err)
}
return snapshot, nil
}
// decodeSchemaCatalogSnapshot is the single release and generation-time
// loading path. Delivery validation round-trips generated JSON through this
// function so it cannot pass with data that the shipped binary would reject.
func decodeSchemaCatalogSnapshot(data []byte) (loadedSchemaCatalog, error) {
var snapshot SchemaCatalogSnapshot
if err := decodeStrictSchemaJSON(data, &snapshot); err != nil {
return loadedSchemaCatalog{}, fmt.Errorf("decode Schema Catalog snapshot: %w", err)
}
return loadSchemaCatalogSnapshot(snapshot)
}
// loadSchemaCatalogSnapshot constructs the production lookup from an
// arbitrary decoded snapshot. It validates the progressive summaries against
// the full leaf store before publishing any lookup key.
func loadSchemaCatalogSnapshot(snapshot SchemaCatalogSnapshot) (loadedSchemaCatalog, error) {
if snapshot.Version != SchemaCatalogSnapshotVersion {
return loadedSchemaCatalog{}, fmt.Errorf("unsupported Schema Catalog snapshot version %d", snapshot.Version)
}
if len(snapshot.Catalog) == 0 || len(snapshot.Tools) == 0 {
return loadedSchemaCatalog{}, fmt.Errorf("schema Catalog snapshot is empty")
}
if snapshot.SourceHash == "" || snapshot.SourceHash != schemaCatalogSnapshotHash(snapshot) {
return loadedSchemaCatalog{}, fmt.Errorf("schema Catalog snapshot source_hash does not match its content")
}
registry, index, err := schemaRegistryFromSnapshot(snapshot)
if err != nil {
return loadedSchemaCatalog{}, fmt.Errorf("load typed Schema registry: %w", err)
}
if err := loadCatalogValidateInterfaces(registry); err != nil {
return loadedSchemaCatalog{}, fmt.Errorf("validate final Schema interface disposition: %w", err)
}
// The production loader validates delivered provenance exactly as encoded.
// toolSpecFromSnapshot deliberately does not synthesize candidates or
// rewrite winners, and this coverage gate applies to every snapshot source.
if err := loadCatalogValidateProvenance(registry); err != nil {
return loadedSchemaCatalog{}, fmt.Errorf("validate final Schema provenance: %w", err)
}
if registry.Source == "embedded-command-catalog" {
if err := loadCatalogValidateAgentMetadata(registry); err != nil {
return loadedSchemaCatalog{}, fmt.Errorf("validate final Schema Agent metadata set: %w", err)
}
}
return loadedSchemaCatalog{Snapshot: snapshot, Registry: registry, Index: index}, nil
}
func embeddedSchemaCatalogAvailable() bool {
return len(embeddedSchemaCatalog().Index.CanonicalPaths()) > 0
}
func embeddedSchemaAllPayload() (map[string]any, error) {
loaded := embeddedSchemaCatalog()
payload, err := renderEmbeddedSchemaAll(loaded.Registry)
if err != nil {
return nil, err
}
payload["catalog_hash"] = loaded.Snapshot.SourceHash
if loaded.Snapshot.SurfaceHash != "" {
payload["surface_hash"] = loaded.Snapshot.SurfaceHash
}
return payload, nil
}
func embeddedSchemaOverviewPayload() (map[string]any, error) {
loaded := embeddedSchemaCatalog()
payload, err := renderEmbeddedSchemaOverview(loaded.Registry)
if err != nil {
return nil, err
}
payload["catalog_hash"] = loaded.Snapshot.SourceHash
if loaded.Snapshot.SurfaceHash != "" {
payload["surface_hash"] = loaded.Snapshot.SurfaceHash
}
return payload, nil
}
func exactSchemaCommand(root *cobra.Command, rawPath string) *cobra.Command {
if root == nil {
return nil
}
parts := strings.Fields(strings.TrimSpace(rawPath))
if len(parts) > 0 && parts[0] == root.Name() {
parts = parts[1:]
}
current := root
for _, part := range parts {
var next *cobra.Command
for _, child := range current.Commands() {
if child.Name() == part {
next = child
break
}
}
if next == nil {
return nil
}
current = next
}
if current == root {
return nil
}
return current
}
func embeddedSchemaPayload(args []string) (map[string]any, error) {
return schemaPayloadFromLoadedCatalog(embeddedSchemaCatalog(), args)
}
// schemaPayloadFromLoadedCatalog is shared by the shipped schema command and
// the final-delivery gate. Keeping lookup and payload rendering on one path
// prevents generation-only validation from accepting an unqueryable snapshot.
func schemaPayloadFromLoadedCatalog(loaded loadedSchemaCatalog, args []string) (map[string]any, error) {
if len(args) == 0 {
snapshot, err := loaded.Registry.ToSnapshotPayload()
if err != nil {
return nil, err
}
payload := snapshot.Catalog
payload["catalog_hash"] = loaded.Snapshot.SourceHash
if loaded.Snapshot.SurfaceHash != "" {
payload["surface_hash"] = loaded.Snapshot.SurfaceHash
}
return payload, nil
}
raw := strings.TrimSpace(args[0])
if tool, ok := loaded.Index.ResolveQuery(raw); ok {
return schemaToolForResolvedPath(tool, raw).ToPayload()
}
tokens := splitSchemaPathTokens(raw)
if len(tokens) == 1 {
if product, ok := loaded.Index.Product(tokens[0]); ok {
payload, err := renderSchemaProductSummary(product)
if err != nil {
return nil, err
}
return map[string]any{
"kind": "schema",
"level": "product",
"count": len(product.Tools),
"product": payload,
"source": "embedded-command-catalog",
}, nil
}
}
if len(tokens) > 1 {
path := strings.Join(tokens, " ")
if product, ok := loaded.Index.Product(tokens[0]); ok {
matched := make([]map[string]any, 0)
for _, tool := range product.Tools {
if schemaToolUnderGroup(tool, path) {
summary, err := renderSchemaToolSummary(tool)
if err != nil {
return nil, err
}
matched = append(matched, summary)
}
}
if len(matched) > 0 {
return map[string]any{
"kind": "schema",
"level": "group",
"path": path,
"count": len(matched),
"tools": matched,
"source": "embedded-command-catalog",
}, nil
}
}
}
return nil, apperrors.NewValidation("unknown runtime schema path " + strconvQuote(raw))
}
func schemaCatalogSnapshotHash(snapshot SchemaCatalogSnapshot) string {
payload := struct {
Version int `json:"version"`
SurfaceHash string `json:"surface_hash,omitempty"`
Catalog map[string]any `json:"catalog"`
Tools map[string]map[string]any `json:"tools"`
}{snapshot.Version, snapshot.SurfaceHash, snapshot.Catalog, snapshot.Tools}
encoded, _ := json.Marshal(payload)
sum := sha256.Sum256(encoded)
return "sha256:" + hex.EncodeToString(sum[:])
}
func schemaMapSlice(value any) []map[string]any {
switch values := value.(type) {
case []map[string]any:
return values
case []any:
out := make([]map[string]any, 0, len(values))
for _, value := range values {
if item, ok := value.(map[string]any); ok {
out = append(out, item)
}
}
return out
default:
return nil
}
}
func schemaMap(value any) map[string]map[string]any {
input, ok := value.(map[string]any)
if !ok {
return nil
}
out := make(map[string]map[string]any, len(input))
for key, value := range input {
if item, ok := value.(map[string]any); ok {
out[key] = item
}
}
return out
}
func schemaString(value any) string {
valueString, _ := value.(string)
return valueString
}
func schemaStringSlice(value any) []string {
switch values := value.(type) {
case []string:
return values
case []any:
out := make([]string, 0, len(values))
for _, value := range values {
if item, ok := value.(string); ok {
out = append(out, item)
}
}
return out
default:
return nil
}
}
func firstNonEmptySchemaString(values ...any) string {
for _, value := range values {
if text := strings.TrimSpace(schemaString(value)); text != "" {
return text
}
}
return ""
}