648 lines
20 KiB
Go
648 lines
20 KiB
Go
package openai
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"iop/packages/go/config"
|
|
)
|
|
|
|
// workspaceOperationKind enumerates the canonical workspace operations the
|
|
// binding compiler can encode. The Edge never executes these; it only produces
|
|
// deterministic, caller-executed payloads from the preset-declared contract.
|
|
type workspaceOperationKind string
|
|
|
|
const (
|
|
opKindPrepare workspaceOperationKind = "prepare"
|
|
opKindRead workspaceOperationKind = "read"
|
|
opKindWrite workspaceOperationKind = "write"
|
|
opKindDelete workspaceOperationKind = "delete"
|
|
)
|
|
|
|
// canonicalOperationOrder is the deterministic order in which an alternative's
|
|
// operations are compiled and fingerprinted. It never depends on Go map
|
|
// iteration order.
|
|
var canonicalOperationOrder = []workspaceOperationKind{opKindPrepare, opKindRead, opKindWrite, opKindDelete}
|
|
|
|
// workspaceBindingMode selects how a compiled operation maps tool arguments.
|
|
//
|
|
// structured: the actual tool exposes the workspace fields by name; the codec
|
|
// maps the configured argument fields directly and preserves typed values.
|
|
//
|
|
// command: the actual tool takes a synthesized command; the codec builds a
|
|
// deterministic, shell-safe command from a fixed argv template.
|
|
type workspaceBindingMode string
|
|
|
|
const (
|
|
modeStructured workspaceBindingMode = "structured"
|
|
modeCommand workspaceBindingMode = "command"
|
|
)
|
|
|
|
// workspaceToolSchema is the normalized view of one decoded tool definition. It
|
|
// accepts OpenAI Chat function wrappers, flat OpenAI parameters, and Anthropic
|
|
// input_schema shapes and exposes a single JSON Schema object for matching.
|
|
type workspaceToolSchema struct {
|
|
name string
|
|
description string
|
|
// schema is the full JSON Schema object (function.parameters / parameters /
|
|
// input_schema). It is matched against the configured schema_matcher.
|
|
schema map[string]any
|
|
// properties is the resolved property set (oneOf/anyOf/allOf aware) used to
|
|
// validate that mapped argument fields are actually declared by the tool.
|
|
properties map[string]any
|
|
}
|
|
|
|
// workspaceOperationBinding is the immutable compiled mapping for one canonical
|
|
// operation of a selected alternative.
|
|
type workspaceOperationBinding struct {
|
|
op workspaceOperationKind
|
|
toolName string
|
|
mode workspaceBindingMode
|
|
// Structured-mode actual argument field names (dot paths permitted).
|
|
pathField string
|
|
contentField string
|
|
modeField string
|
|
// Command-mode encoding.
|
|
commandField string
|
|
argvTemplate []string
|
|
// Immutable copies of the configured contract for this operation.
|
|
schemaMatcher map[string]any
|
|
argumentMap map[string]any
|
|
resultMatcher map[string]any
|
|
createsParents bool
|
|
// normalizedSchema is the actual tool schema this operation bound to.
|
|
normalizedSchema *workspaceToolSchema
|
|
}
|
|
|
|
// workspaceBinding is the immutable, fingerprinted selection of exactly one
|
|
// complete configured alternative. It carries every operation mapping and the
|
|
// parent-creation capability, and the Edge never mutates it after selection.
|
|
type workspaceBinding struct {
|
|
alternativeName string
|
|
operations map[workspaceOperationKind]*workspaceOperationBinding
|
|
// fingerprint is a sha256 of the canonical selected configuration plus the
|
|
// normalized actual schemas. It correlates results back to this binding.
|
|
fingerprint string
|
|
}
|
|
|
|
// compileWorkspaceBinding selects the first configured alternative whose every
|
|
// declared operation matches an actual decoded tool by exact tool name and
|
|
// recursive schema matcher. It never infers workspace roles from tool-name
|
|
// substrings and never inspects the workspace filesystem.
|
|
//
|
|
// It returns an immutable, fully-mapped binding, or nil with an error that
|
|
// explains why no complete alternative matched.
|
|
func compileWorkspaceBinding(alternatives []config.ExecutionWorkspaceToolAlternative, tools any) (*workspaceBinding, error) {
|
|
if len(alternatives) == 0 {
|
|
return nil, fmt.Errorf("no configured workspace tool alternatives")
|
|
}
|
|
schemasByName, err := normalizeToolSchemas(tools)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var lastErr error
|
|
for _, alt := range alternatives {
|
|
binding, err := bindAlternative(alt, schemasByName)
|
|
if err != nil {
|
|
lastErr = err
|
|
continue
|
|
}
|
|
return binding, nil
|
|
}
|
|
if lastErr == nil {
|
|
lastErr = fmt.Errorf("no workspace tool alternative matched the provided tools")
|
|
}
|
|
return nil, lastErr
|
|
}
|
|
|
|
// normalizeToolSchemas normalizes every decoded tool definition into a schema
|
|
// view keyed by its exact tool name. Tools without a name are ignored; the
|
|
// first definition wins on duplicate names. It handles OpenAI Chat nested
|
|
// function wrappers, flat OpenAI parameters, and Anthropic input_schema shapes.
|
|
func normalizeToolSchemas(tools any) (map[string]*workspaceToolSchema, error) {
|
|
var entries []any
|
|
switch typed := tools.(type) {
|
|
case []any:
|
|
entries = typed
|
|
case []anthropicTool:
|
|
entries = make([]any, len(typed))
|
|
for i, tool := range typed {
|
|
entries[i] = tool
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("unsupported workspace tool slice type %T", tools)
|
|
}
|
|
|
|
out := make(map[string]*workspaceToolSchema, len(entries))
|
|
for _, rawTool := range entries {
|
|
schema := extractToolSchema(rawTool)
|
|
if schema == nil {
|
|
continue
|
|
}
|
|
if _, exists := out[schema.name]; exists {
|
|
continue
|
|
}
|
|
out[schema.name] = schema
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// extractToolSchema pulls the normalized schema from a single tool entry. It
|
|
// recognizes the actual OpenAI Chat function wrapper
|
|
// ({type:"function",function:{name,description,parameters}}), the flat OpenAI
|
|
// shape ({name,parameters}), and the Anthropic shape ({name,input_schema}).
|
|
func extractToolSchema(rawTool any) *workspaceToolSchema {
|
|
switch tool := rawTool.(type) {
|
|
case map[string]any:
|
|
return extractMappedToolSchema(tool)
|
|
case anthropicTool:
|
|
return extractAnthropicToolSchema(tool)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func extractMappedToolSchema(m map[string]any) *workspaceToolSchema {
|
|
name, _ := m["name"].(string)
|
|
desc, _ := m["description"].(string)
|
|
|
|
var schemaObj map[string]any
|
|
|
|
// OpenAI Chat nested function wrapper.
|
|
if fn, ok := m["function"].(map[string]any); ok {
|
|
if name == "" {
|
|
name, _ = fn["name"].(string)
|
|
}
|
|
if desc == "" {
|
|
desc, _ = fn["description"].(string)
|
|
}
|
|
if params, ok := fn["parameters"].(map[string]any); ok {
|
|
schemaObj = params
|
|
}
|
|
}
|
|
// Anthropic input_schema.
|
|
if schemaObj == nil {
|
|
if s, ok := m["input_schema"].(map[string]any); ok {
|
|
schemaObj = s
|
|
}
|
|
}
|
|
// Flat OpenAI parameters.
|
|
if schemaObj == nil {
|
|
if s, ok := m["parameters"].(map[string]any); ok {
|
|
schemaObj = s
|
|
}
|
|
}
|
|
|
|
if strings.TrimSpace(name) == "" {
|
|
return nil
|
|
}
|
|
return &workspaceToolSchema{
|
|
name: name,
|
|
description: desc,
|
|
schema: schemaObj,
|
|
properties: schemaObjectProperties(schemaObj),
|
|
}
|
|
}
|
|
|
|
// extractAnthropicToolSchema normalizes the concrete native Messages decoder
|
|
// value. InputSchema is deliberately decoded into a new map so a binding does
|
|
// not retain the request's RawMessage buffer or infer a role by reflection.
|
|
func extractAnthropicToolSchema(tool anthropicTool) *workspaceToolSchema {
|
|
if strings.TrimSpace(tool.Name) == "" || len(tool.InputSchema) == 0 {
|
|
return nil
|
|
}
|
|
decoder := json.NewDecoder(bytes.NewReader(tool.InputSchema))
|
|
decoder.UseNumber()
|
|
var schema map[string]any
|
|
if err := decoder.Decode(&schema); err != nil || schema == nil {
|
|
return nil
|
|
}
|
|
var trailing any
|
|
if err := decoder.Decode(&trailing); err != io.EOF {
|
|
return nil
|
|
}
|
|
return &workspaceToolSchema{
|
|
name: tool.Name,
|
|
description: tool.Description,
|
|
schema: cloneAnyMap(schema),
|
|
properties: schemaObjectProperties(schema),
|
|
}
|
|
}
|
|
|
|
// bindAlternative compiles a single configured alternative against the
|
|
// normalized actual tools. Every declared operation must bind, and the
|
|
// alternative must satisfy write-with-parents or separate-prepare completeness.
|
|
func bindAlternative(alt config.ExecutionWorkspaceToolAlternative, schemasByName map[string]*workspaceToolSchema) (*workspaceBinding, error) {
|
|
name := strings.TrimSpace(alt.Name)
|
|
ops := make(map[workspaceOperationKind]*workspaceOperationBinding, len(alt.Operations))
|
|
for _, kind := range canonicalOperationOrder {
|
|
cfgOp, ok := alt.Operations[string(kind)]
|
|
if !ok {
|
|
continue
|
|
}
|
|
opBinding, err := bindOperation(kind, cfgOp, schemasByName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("alternative %q operation %q: %w", name, kind, err)
|
|
}
|
|
ops[kind] = opBinding
|
|
}
|
|
if len(ops) == 0 {
|
|
return nil, fmt.Errorf("alternative %q declares no recognized operations", name)
|
|
}
|
|
if err := validateAlternativeCompleteness(name, ops); err != nil {
|
|
return nil, err
|
|
}
|
|
binding := &workspaceBinding{alternativeName: name, operations: ops}
|
|
binding.fingerprint = computeBindingFingerprint(binding)
|
|
return binding, nil
|
|
}
|
|
|
|
// validateAlternativeCompleteness enforces the write-with-parents or
|
|
// separate-prepare completeness invariant: a write operation that cannot create
|
|
// missing parents requires a prepare operation in the same alternative.
|
|
func validateAlternativeCompleteness(name string, ops map[workspaceOperationKind]*workspaceOperationBinding) error {
|
|
write, hasWrite := ops[opKindWrite]
|
|
if hasWrite && !write.createsParents {
|
|
if _, hasPrepare := ops[opKindPrepare]; !hasPrepare {
|
|
return fmt.Errorf("alternative %q: write cannot create parents and no prepare operation is declared", name)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// bindOperation binds one configured operation to its actual tool by exact name
|
|
// and recursive schema matcher, resolves the argument map, validates mapped
|
|
// fields against the actual schema, and copies the immutable result matcher.
|
|
func bindOperation(kind workspaceOperationKind, cfgOp config.ExecutionWorkspaceOperation, schemasByName map[string]*workspaceToolSchema) (*workspaceOperationBinding, error) {
|
|
toolName := strings.TrimSpace(cfgOp.ToolName)
|
|
if toolName == "" {
|
|
return nil, fmt.Errorf("tool_name must not be empty")
|
|
}
|
|
schema, ok := schemasByName[toolName]
|
|
if !ok {
|
|
return nil, fmt.Errorf("tool %q is not present in the request tools", toolName)
|
|
}
|
|
if len(cfgOp.SchemaMatcher) == 0 {
|
|
return nil, fmt.Errorf("schema_matcher must not be empty")
|
|
}
|
|
if !schemaMatcherMatches(cfgOp.SchemaMatcher, schema.schema) {
|
|
return nil, fmt.Errorf("tool %q schema does not satisfy the configured schema_matcher", toolName)
|
|
}
|
|
if len(cfgOp.ArgumentMap) == 0 {
|
|
return nil, fmt.Errorf("argument_map must not be empty")
|
|
}
|
|
if len(cfgOp.ResultMatcher) == 0 {
|
|
return nil, fmt.Errorf("result_matcher must not be empty")
|
|
}
|
|
ob := &workspaceOperationBinding{
|
|
op: kind,
|
|
toolName: toolName,
|
|
schemaMatcher: cloneAnyMap(cfgOp.SchemaMatcher),
|
|
argumentMap: cloneAnyMap(cfgOp.ArgumentMap),
|
|
resultMatcher: cloneAnyMap(cfgOp.ResultMatcher),
|
|
createsParents: cfgOp.CreatesParents,
|
|
normalizedSchema: cloneWorkspaceToolSchema(schema),
|
|
}
|
|
if err := resolveArgumentMap(ob, kind); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := validateMappedFields(ob, schema); err != nil {
|
|
return nil, err
|
|
}
|
|
return ob, nil
|
|
}
|
|
|
|
// cloneWorkspaceToolSchema detaches the compiled binding from the request's
|
|
// decoded tool map. A caller can reuse or mutate its decoded request after
|
|
// admission, but that must not alter the request-local binding contract.
|
|
func cloneWorkspaceToolSchema(schema *workspaceToolSchema) *workspaceToolSchema {
|
|
if schema == nil {
|
|
return nil
|
|
}
|
|
return &workspaceToolSchema{
|
|
name: schema.name,
|
|
description: schema.description,
|
|
schema: cloneAnyMap(schema.schema),
|
|
properties: cloneAnyMap(schema.properties),
|
|
}
|
|
}
|
|
|
|
// resolveArgumentMap interprets the configured argument_map into structured or
|
|
// command encoding fields. The presence of a "command" field name selects
|
|
// command mode. A "path" mapping is always required; write additionally
|
|
// requires a "content" mapping.
|
|
func resolveArgumentMap(ob *workspaceOperationBinding, kind workspaceOperationKind) error {
|
|
am := ob.argumentMap
|
|
pathField, ok := stringField(am, "path")
|
|
if !ok {
|
|
return fmt.Errorf("argument_map requires a non-empty %q field name", "path")
|
|
}
|
|
ob.pathField = pathField
|
|
if content, ok := stringField(am, "content"); ok {
|
|
ob.contentField = content
|
|
}
|
|
if modeField, ok := stringField(am, "mode"); ok {
|
|
ob.modeField = modeField
|
|
}
|
|
|
|
if command, ok := stringField(am, "command"); ok {
|
|
ob.mode = modeCommand
|
|
ob.commandField = command
|
|
argv, err := parseArgvTemplate(am["argv"])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
placeholders, err := validateCommandArgvTemplate(argv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if placeholders["{path}"] != 1 {
|
|
return fmt.Errorf("command argv template must reference the {path} placeholder exactly once")
|
|
}
|
|
if kind == opKindWrite && placeholders["{content}"] != 1 {
|
|
return fmt.Errorf("write command argv template must reference the {content} placeholder exactly once")
|
|
}
|
|
ob.argvTemplate = argv
|
|
} else {
|
|
ob.mode = modeStructured
|
|
}
|
|
|
|
if kind == opKindWrite && ob.contentField == "" {
|
|
return fmt.Errorf("write argument_map requires a non-empty %q field name", "content")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateMappedFields ties the argument map to the actual tool schema. In
|
|
// structured mode every mapped field must be declared by the schema; in command
|
|
// mode the synthesized command field must be declared by the schema.
|
|
func validateMappedFields(ob *workspaceOperationBinding, schema *workspaceToolSchema) error {
|
|
check := func(role, field string) error {
|
|
if field == "" {
|
|
return nil
|
|
}
|
|
root := strings.SplitN(field, ".", 2)[0]
|
|
if _, ok := schema.properties[root]; !ok {
|
|
return fmt.Errorf("mapped %s field %q is not declared by tool %q schema", role, field, schema.name)
|
|
}
|
|
return nil
|
|
}
|
|
switch ob.mode {
|
|
case modeStructured:
|
|
if err := check("path", ob.pathField); err != nil {
|
|
return err
|
|
}
|
|
if err := check("content", ob.contentField); err != nil {
|
|
return err
|
|
}
|
|
if err := check("mode", ob.modeField); err != nil {
|
|
return err
|
|
}
|
|
case modeCommand:
|
|
if err := check("command", ob.commandField); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// schemaMatcherMatches reports whether the actual tool schema satisfies the
|
|
// configured recursive schema matcher (a deep subset match).
|
|
func schemaMatcherMatches(matcher map[string]any, schema map[string]any) bool {
|
|
if schema == nil {
|
|
schema = map[string]any{}
|
|
}
|
|
return deepSubsetMatch(map[string]any(matcher), map[string]any(schema))
|
|
}
|
|
|
|
// deepSubsetMatch reports whether actual contains everything declared by
|
|
// matcher. Maps match as subsets, slices require each matcher element to be
|
|
// found in actual, and scalars compare by value. A small operator vocabulary
|
|
// is supported for string matcher leaves: "$any", "$string", "$number",
|
|
// "$bool".
|
|
func deepSubsetMatch(matcher, actual any) bool {
|
|
switch m := matcher.(type) {
|
|
case map[string]any:
|
|
am, ok := actual.(map[string]any)
|
|
if !ok {
|
|
return false
|
|
}
|
|
for key, mv := range m {
|
|
av, ok := am[key]
|
|
if !ok {
|
|
return false
|
|
}
|
|
if !deepSubsetMatch(mv, av) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
case []any:
|
|
as, ok := actual.([]any)
|
|
if !ok {
|
|
return false
|
|
}
|
|
for _, mv := range m {
|
|
found := false
|
|
for _, av := range as {
|
|
if deepSubsetMatch(mv, av) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
case string:
|
|
switch m {
|
|
case "$any":
|
|
return actual != nil
|
|
case "$string":
|
|
_, ok := actual.(string)
|
|
return ok
|
|
case "$number":
|
|
_, ok := toFloat(actual)
|
|
return ok
|
|
case "$bool":
|
|
_, ok := actual.(bool)
|
|
return ok
|
|
}
|
|
s, ok := actual.(string)
|
|
return ok && s == m
|
|
default:
|
|
return valuesEqual(matcher, actual)
|
|
}
|
|
}
|
|
|
|
// valuesEqual compares two scalar values, normalizing numeric types so that a
|
|
// config int and a decoded json.Number/float64 compare equal.
|
|
func valuesEqual(a, b any) bool {
|
|
if af, ok := toFloat(a); ok {
|
|
if bf, ok := toFloat(b); ok {
|
|
return af == bf
|
|
}
|
|
return false
|
|
}
|
|
return reflect.DeepEqual(a, b)
|
|
}
|
|
|
|
// toFloat converts any supported numeric representation to a float64.
|
|
func toFloat(v any) (float64, bool) {
|
|
switch n := v.(type) {
|
|
case float64:
|
|
return n, true
|
|
case float32:
|
|
return float64(n), true
|
|
case int:
|
|
return float64(n), true
|
|
case int32:
|
|
return float64(n), true
|
|
case int64:
|
|
return float64(n), true
|
|
case json.Number:
|
|
if f, err := n.Float64(); err == nil {
|
|
return f, true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
// computeBindingFingerprint produces a deterministic sha256 hex digest of the
|
|
// selected alternative's canonical configuration plus the normalized actual
|
|
// schemas. json.Marshal sorts object keys, so the digest is stable regardless
|
|
// of Go map iteration order or endpoint tool-definition shape.
|
|
func computeBindingFingerprint(b *workspaceBinding) string {
|
|
opsDesc := make(map[string]any, len(b.operations))
|
|
for kind, ob := range b.operations {
|
|
var normalizedSchema any
|
|
if ob.normalizedSchema != nil {
|
|
normalizedSchema = ob.normalizedSchema.schema
|
|
}
|
|
opsDesc[string(kind)] = map[string]any{
|
|
"tool_name": ob.toolName,
|
|
"mode": string(ob.mode),
|
|
"schema_matcher": ob.schemaMatcher,
|
|
"argument_map": ob.argumentMap,
|
|
"result_matcher": ob.resultMatcher,
|
|
"creates_parents": ob.createsParents,
|
|
"normalized_schema": normalizedSchema,
|
|
}
|
|
}
|
|
desc := map[string]any{
|
|
"alternative": b.alternativeName,
|
|
"operations": opsDesc,
|
|
}
|
|
raw, _ := json.Marshal(desc)
|
|
sum := sha256.Sum256(raw)
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
// stringField returns a trimmed non-empty string value for key, or false.
|
|
func stringField(m map[string]any, key string) (string, bool) {
|
|
v, ok := m[key]
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
s, ok := v.(string)
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
return "", false
|
|
}
|
|
return s, true
|
|
}
|
|
|
|
// parseArgvTemplate validates and copies the command argv template.
|
|
func parseArgvTemplate(v any) ([]string, error) {
|
|
raw, ok := v.([]any)
|
|
if !ok || len(raw) == 0 {
|
|
return nil, fmt.Errorf("command argument_map requires a non-empty %q template array", "argv")
|
|
}
|
|
out := make([]string, 0, len(raw))
|
|
for i, item := range raw {
|
|
s, ok := item.(string)
|
|
if !ok {
|
|
return nil, fmt.Errorf("argv template token %d is not a string", i)
|
|
}
|
|
out = append(out, s)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// validateCommandArgvTemplate permits placeholders only as whole argv tokens.
|
|
// This makes command mapping unambiguous: Edge determines exactly which argv
|
|
// element receives each canonical value instead of accepting shell fragments or
|
|
// unsupported interpolation syntax.
|
|
func validateCommandArgvTemplate(argv []string) (map[string]int, error) {
|
|
counts := make(map[string]int, 2)
|
|
for _, token := range argv {
|
|
switch token {
|
|
case "{path}", "{content}":
|
|
counts[token]++
|
|
default:
|
|
if strings.ContainsAny(token, "{}") {
|
|
return nil, fmt.Errorf("command argv template has unsupported placeholder token %q", token)
|
|
}
|
|
}
|
|
}
|
|
return counts, nil
|
|
}
|
|
|
|
// cloneAnyMap deep-copies a decoded JSON map so the compiled binding is
|
|
// independent of later config mutation.
|
|
func cloneAnyMap(m map[string]any) map[string]any {
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
out := make(map[string]any, len(m))
|
|
for k, v := range m {
|
|
out[k] = cloneAnyValue(v)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func cloneAnyValue(v any) any {
|
|
switch t := v.(type) {
|
|
case map[string]any:
|
|
out := make(map[string]any, len(t))
|
|
for k, vv := range t {
|
|
out[k] = cloneAnyValue(vv)
|
|
}
|
|
return out
|
|
case []any:
|
|
out := make([]any, len(t))
|
|
for i, vv := range t {
|
|
out[i] = cloneAnyValue(vv)
|
|
}
|
|
return out
|
|
default:
|
|
return t
|
|
}
|
|
}
|
|
|
|
// bindingFingerprint returns the immutable binding fingerprint.
|
|
func (b *workspaceBinding) bindingFingerprint() string { return b.fingerprint }
|
|
|
|
// operation returns the compiled operation binding for kind, or nil.
|
|
func (b *workspaceBinding) operation(kind workspaceOperationKind) *workspaceOperationBinding {
|
|
return b.operations[kind]
|
|
}
|
|
|
|
// createsParents reports whether the selected write operation creates missing
|
|
// parents. It returns false when the binding has no write operation.
|
|
func (b *workspaceBinding) createsParents() bool {
|
|
if write, ok := b.operations[opKindWrite]; ok {
|
|
return write.createsParents
|
|
}
|
|
return false
|
|
}
|