Files
dingtalk-workspace-cli/internal/cli/schema_catalog.go
T
玉澜andCursor 9bf772ea06 chore(schema): rename Embedded/Hint leftovers to delivery/selection
Drop misleading Catalog-embed and HintFile naming now that assembly is
declare→delivery and selection comes from ContractFinal.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-02 13:47:26 +08:00

570 lines
22 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"
"encoding/hex"
"encoding/json"
"fmt"
"io/fs"
"strings"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/spf13/cobra"
)
const SchemaCatalogSnapshotVersion = 1
// Schema Catalog delivery is single-track: RegisterSchemaSourceRoot →
// ResolveSchemaBuild (see schema_source_root.go). There is no committed
// schema_catalog/ embed and no gob/Catalog fixture fallback. Shard assemble
// helpers below exist for CI dumps (cmd_schema_catalog) and synthetic unit
// tests that pass explicit envelope/FS bytes.
// 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
}
func registryToSnapshotPayload(registry SchemaRegistry) (SchemaCatalogSnapshot, error) {
payload, err := registry.ToSnapshotPayload()
if err != nil {
return SchemaCatalogSnapshot{}, err
}
return SchemaCatalogSnapshot{Catalog: payload.Catalog, Tools: payload.Tools}, nil
}
var registryToSnapshotPayloadFn = registryToSnapshotPayload
// 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"`
}
// schemaCatalogEnvelopeTyped is the production cold-start envelope: Catalog is
// decoded straight into wire structs instead of map[string]any.
type schemaCatalogEnvelopeTyped struct {
Version int `json:"version"`
SurfaceHash string `json:"surface_hash,omitempty"`
SourceHash string `json:"source_hash"`
Catalog schemaCatalogWire `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"`
}
// schemaCatalogToolShardTyped is the production cold-start shard: each ToolSpec
// decodes directly into schemaToolWire with no map[string]any intermediate.
type schemaCatalogToolShardTyped struct {
Product string `json:"product"`
Tools map[string]schemaToolWire `json:"tools"`
}
// assembleTypedSchemaCatalog decodes the embedded envelope and per-product
// shards straight into wire structs for the production loader.
func assembleTypedSchemaCatalog(envelopeJSON []byte, shards fs.FS, dir string) (schemaCatalogEnvelopeTyped, map[string]schemaToolWire, error) {
var envelope schemaCatalogEnvelopeTyped
if err := decodeStrictSchemaJSON(envelopeJSON, &envelope); err != nil {
return schemaCatalogEnvelopeTyped{}, nil, fmt.Errorf("decode schema catalog.json: %w", err)
}
entries, err := fs.ReadDir(shards, dir)
if err != nil {
return schemaCatalogEnvelopeTyped{}, nil, fmt.Errorf("read schema catalog tools directory: %w", err)
}
tools := make(map[string]schemaToolWire, 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 schemaCatalogEnvelopeTyped{}, nil, fmt.Errorf("read schema catalog shard %s: %w", entry.Name(), readErr)
}
var shard schemaCatalogToolShardTyped
if err := decodeStrictSchemaJSON(data, &shard); err != nil {
return schemaCatalogEnvelopeTyped{}, nil, fmt.Errorf("decode schema catalog shard %s: %w", entry.Name(), err)
}
for canonical, spec := range shard.Tools {
tools[canonical] = spec
}
}
return envelope, tools, nil
}
func loadTypedSchemaCatalog(envelope schemaCatalogEnvelopeTyped, tools map[string]schemaToolWire) (loadedSchemaCatalog, error) {
if envelope.Version != SchemaCatalogSnapshotVersion {
return loadedSchemaCatalog{}, fmt.Errorf("unsupported Schema Catalog snapshot version %d", envelope.Version)
}
if envelope.Catalog.Kind == "" || len(tools) == 0 {
return loadedSchemaCatalog{}, fmt.Errorf("schema Catalog snapshot is empty")
}
if envelope.SourceHash == "" {
return loadedSchemaCatalog{}, fmt.Errorf("schema Catalog snapshot is missing source_hash")
}
registry, index, err := schemaRegistryFromTyped(envelope.Catalog, tools)
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)
}
if err := loadCatalogValidateProvenance(registry); err != nil {
return loadedSchemaCatalog{}, fmt.Errorf("validate final Schema provenance: %w", err)
}
return loadedSchemaCatalog{
Snapshot: SchemaCatalogSnapshot{
Version: envelope.Version,
SurfaceHash: envelope.SurfaceHash,
SourceHash: envelope.SourceHash,
},
Registry: registry,
Index: index,
}, nil
}
// assembleSchemaCatalogSnapshot merges an envelope document and a directory of
// per-product tool shards back into the single-document snapshot shape used by
// ephemeral cmd_schema_catalog dumps. 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 schema catalog.json: %w", err)
}
entries, err := fs.ReadDir(shards, dir)
if err != nil {
return SchemaCatalogSnapshot{}, fmt.Errorf("read 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 schema catalog shard %s: %w", entry.Name(), readErr)
}
var shard schemaCatalogToolShard
if err := decodeStrictSchemaJSON(data, &shard); err != nil {
return SchemaCatalogSnapshot{}, fmt.Errorf("decode 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
}
var (
buildCatalogValidateParameterBindings = ValidateSchemaParameterBindingDelivery
buildCatalogValidateDryRun = ValidateReviewedDryRunCapabilityDelivery
buildCatalogValidateExamples = ValidateAgentExampleDelivery
buildCatalogValidateCompleteness = validateResolvedRuntimeSchemaCompleteness
buildCatalogValidateRegistry = validateSchemaRegistryAgainstCommandRegistry
buildCatalogValidateInterfaces = validateSchemaRegistryInterfaces
buildCatalogValidateAgentMetadata = validateSchemaRegistryAgentMetadata
buildCatalogValidateProvenance = validateFinalSchemaProvenanceCoverage
buildCatalogValidateDelivery = ValidateSchemaDeliveryInvariants
buildCatalogValidateFinalCompleteness = validateResolvedSchemaCatalogDeliveryCompleteness
loadCatalogValidateInterfaces = validateSchemaRegistryInterfaces
loadCatalogValidateProvenance = validateFinalSchemaProvenanceCoverage
renderDeliverySchemaAll = func(registry SchemaRegistry) (map[string]any, error) { return registry.ToPayload() }
renderDeliverySchemaOverview = 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 = SchemaSourceRuntimeAssembled
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 == "" {
return loadedSchemaCatalog{}, fmt.Errorf("schema Catalog snapshot is missing source_hash")
}
// source_hash equality is a build-time property, enforced by
// check-generated-drift.sh, which regenerates and byte-compares the whole
// artifact set — strictly stronger than this hash, since it also catches a
// shard set that is self-consistent but stale. Recomputing it here re-marshals
// every decoded map (measured: 86.9ms, 65MB, 997k allocs) on each full
// Catalog load, to re-derive a fact about an embedded artifact that cannot
// change at runtime. ResolveMeta no longer loads this path.
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)
}
// Agent metadata is no longer a shipped embed. CI/local cmd_schema_catalog
// dumps may still inject Agent metadata for validation; production delivery
// reassembles via ResolveSchemaBuild and must not reopen retired
// schema_agent_metadata/ artifacts.
return loadedSchemaCatalog{Snapshot: snapshot, Registry: registry, Index: index}, nil
}
func deliverySchemaCatalogAvailable() bool {
return deliverySchemaCatalogError() == nil && len(deliverySchemaCatalog().Index.CanonicalPaths()) > 0
}
func deliverySchemaAllPayload() (map[string]any, error) {
if err := deliverySchemaCatalogError(); err != nil {
return nil, err
}
return schemaAllPayloadFromLoaded(deliverySchemaCatalog())
}
func deliverySchemaOverviewPayload() (map[string]any, error) {
if err := deliverySchemaCatalogError(); err != nil {
return nil, err
}
return schemaOverviewPayloadFromLoaded(deliverySchemaCatalog())
}
func schemaAllPayloadFromLoaded(loaded loadedSchemaCatalog) (map[string]any, error) {
payload, err := renderDeliverySchemaAll(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 schemaOverviewPayloadFromLoaded(loaded loadedSchemaCatalog) (map[string]any, error) {
payload, err := renderDeliverySchemaOverview(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
}
// queryDeliverySchemaPayload serves dws schema queries through the production
// delivery loader. Completeness / invariant gates must call
// schemaPayloadFromLoadedCatalog (or the deliverySchemaPayload var alias) with
// an explicit loaded catalog — never this helper — to avoid an init cycle
// through assembleSchemaCatalogFromRoot → BuildSchemaCatalogSnapshot.
func queryDeliverySchemaPayload(args []string) (map[string]any, error) {
if err := deliverySchemaCatalogError(); err != nil {
return nil, err
}
return schemaPayloadFromLoadedCatalog(deliverySchemaCatalog(), 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
}
source := strings.TrimSpace(loaded.Registry.Source)
if source == "" {
source = SchemaSourceRuntimeAssembled
}
return map[string]any{
"kind": "schema",
"level": "product",
"count": len(product.Tools),
"product": payload,
"source": source,
}, 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 {
source := strings.TrimSpace(loaded.Registry.Source)
if source == "" {
source = SchemaSourceRuntimeAssembled
}
return map[string]any{
"kind": "schema",
"level": "group",
"path": path,
"count": len(matched),
"tools": matched,
"source": source,
}, 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 ""
}