Files

207 lines
6.4 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 contractfinal
import (
"strings"
"sync"
"github.com/spf13/cobra"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/runtimeannotate"
)
var contractFinalByCommand sync.Map // *cobra.Command → *contract.ContractFinalPayload
// RegisterRuntimeContractFinal annotates dws.schema.contract then stores the
// typed final Schema overlay. This is the atomic annotate+store implementation.
//
// Ownership lives under the command framework (this package). All callers —
// products and corecmd.AttachContract alike — call this function directly.
func RegisterRuntimeContractFinal(cmd *cobra.Command, payload contract.ContractFinalPayload) {
if cmd == nil {
return
}
runtimeannotate.AnnotateRuntimeContract(cmd)
p := cloneContractFinalPayload(payload)
contractFinalByCommand.Store(cmd, &p)
}
// RuntimeContractFinal returns the registered final Schema overlay (read-only).
func RuntimeContractFinal(cmd *cobra.Command) (contract.ContractFinalPayload, bool) {
if cmd == nil {
return contract.ContractFinalPayload{}, false
}
raw, ok := contractFinalByCommand.Load(cmd)
if !ok {
return contract.ContractFinalPayload{}, false
}
p, ok := raw.(*contract.ContractFinalPayload)
if !ok || p == nil {
return contract.ContractFinalPayload{}, false
}
return cloneContractFinalPayload(*p), true
}
func cloneContractFinalPayload(in contract.ContractFinalPayload) contract.ContractFinalPayload {
out := in
out.Positionals = cloneSlice(in.Positionals)
out.Parameters = cloneSlice(in.Parameters)
for i := range out.Parameters {
out.Parameters[i].Enum = cloneSlice(in.Parameters[i].Enum)
if in.Parameters[i].Required != nil {
required := *in.Parameters[i].Required
out.Parameters[i].Required = &required
}
}
if in.Safety != nil {
value := *in.Safety
out.Safety = &value
}
if in.DryRun != nil {
value := *in.DryRun
out.DryRun = &value
}
if in.Result != nil {
value := *in.Result
value.Outcomes = cloneSlice(in.Result.Outcomes)
value.DataSchema = cloneSlice(in.Result.DataSchema)
value.SensitivePaths = cloneSlice(in.Result.SensitivePaths)
out.Result = &value
}
if in.Pagination != nil {
value := *in.Pagination
out.Pagination = &value
}
if in.Interface != nil {
value := *in.Interface
if in.Interface.Ref != nil {
ref := *in.Interface.Ref
value.Ref = &ref
}
out.Interface = &value
}
if in.Selection != nil {
value := *in.Selection
value.UseWhen = cloneSlice(in.Selection.UseWhen)
value.AvoidWhen = cloneSlice(in.Selection.AvoidWhen)
value.Prerequisites = cloneSlice(in.Selection.Prerequisites)
value.Tips = cloneSlice(in.Selection.Tips)
value.WorkflowRefs = cloneSlice(in.Selection.WorkflowRefs)
value.Examples = cloneSlice(in.Selection.Examples)
value.SourceRefs = cloneSlice(in.Selection.SourceRefs)
value.ExampleDispositions = cloneSlice(in.Selection.ExampleDispositions)
for i := range value.ExampleDispositions {
if in.Selection.ExampleDispositions[i].Index != nil {
index := *in.Selection.ExampleDispositions[i].Index
value.ExampleDispositions[i].Index = &index
}
}
if in.Selection.Reviewed != nil {
reviewed := *in.Selection.Reviewed
value.Reviewed = &reviewed
}
out.Selection = &value
}
if in.Identity != nil {
value := *in.Identity
value.Aliases = cloneSlice(in.Identity.Aliases)
out.Identity = &value
}
return out
}
func cloneSlice[T any](in []T) []T {
if in == nil {
return nil
}
out := make([]T, len(in))
copy(out, in)
return out
}
// HasRuntimeContractFinal reports whether the leaf has a registered final overlay.
func HasRuntimeContractFinal(cmd *cobra.Command) bool {
if cmd == nil {
return false
}
_, ok := contractFinalByCommand.Load(cmd)
return ok
}
// ResolveRuntimeSafety finds the live ContractFinal safety declaration for an
// invocation identity. declared distinguishes a matched declaration whose
// safety is unavailable or conflicting from a legacy invocation with no unified
// declaration context. Repeated equivalent command-tree registrations are
// accepted; conflicting matches fail closed with ok=false.
func ResolveRuntimeSafety(canonicalPath, cliPath string) (safety contract.SafetySpec, declared, ok bool) {
canonicalPath = strings.TrimSpace(canonicalPath)
cliPath = strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(cliPath), "dws "))
var resolved contract.SafetySpec
contractFinalByCommand.Range(func(_, raw any) bool {
payload, valid := raw.(*contract.ContractFinalPayload)
if !valid || payload == nil || payload.Identity == nil ||
!runtimeIdentityMatches(*payload.Identity, canonicalPath, cliPath) {
return true
}
declared = true
if payload.Safety == nil {
ok = false
return false
}
candidate := normalizedRuntimeSafety(*payload.Safety)
if !ok {
resolved = candidate
ok = true
return true
}
if resolved != candidate {
ok = false
return false
}
return true
})
return resolved, declared, ok
}
func runtimeIdentityMatches(identity contract.ToolIdentitySpec, canonicalPath, cliPath string) bool {
if canonicalPath != "" {
for _, value := range []string{identity.CanonicalPath, identity.Path} {
if strings.TrimSpace(value) == canonicalPath {
return true
}
}
}
if cliPath == "" {
return false
}
for _, value := range []string{identity.PrimaryCLIPath, identity.CLIPath} {
if strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(value), "dws ")) == cliPath {
return true
}
}
return false
}
func normalizedRuntimeSafety(safety contract.SafetySpec) contract.SafetySpec {
safety.Effect = strings.TrimSpace(safety.Effect)
safety.EffectSource = strings.TrimSpace(safety.EffectSource)
safety.Risk = strings.TrimSpace(safety.Risk)
safety.Confirmation = strings.TrimSpace(safety.Confirmation)
safety.Idempotency = strings.TrimSpace(safety.Idempotency)
return safety
}