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 }