package openai import ( "bytes" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "strings" ) // workspaceEncodedPayload is the deterministic, self-contained payload the Edge // produces for caller execution. The Edge never executes it and never inspects // the workspace; it only produces it from the compiled binding and the issued // tool call. type workspaceEncodedPayload struct { fingerprint string alternative string operation workspaceOperationKind mode workspaceBindingMode toolName string // publicCallID is the IOP-issued tool call id; providerCallID is the // provider-native id. Both are carried into receipt correlation. publicCallID string providerCallID string // safePath is the lexically normalized, containment-checked relative path. safePath string // structuredArgs is the outgoing argument map keyed by actual tool field // names. In structured mode it carries typed values unchanged; in command // mode it carries only the synthesized command field. structuredArgs map[string]any // Command-mode encoding. commandArgv holds the resolved, unquoted argv in // deterministic template order; commandString is its shell-safe joining. commandField string commandArgv []string commandString string // containmentGuard is the caller-executed guard expression. The Edge never // evaluates it; it is returned verbatim to the caller. containmentGuard string // correlationDigest seals the complete issued payload. Receipt matching // recomputes it before trusting any mutable in-memory fields. correlationDigest string } // workspaceResult is a caller-reported workspace operation result the codec // correlates against an issued payload. type workspaceResult struct { // callID is the tool call id the caller reports the result for. It must // equal the issued public or provider id. callID string // status is the caller-reported outcome (e.g. "success", "error"). status string // body is the caller-reported result body, if any. body json.RawMessage } // workspaceResultReceipt records the correlation between a caller-reported // result and the binding/payload that produced the call. type workspaceResultReceipt struct { fingerprint string alternative string operation workspaceOperationKind toolName string publicCallID string providerCallID string path string status string // resultHash is a sha256 of the compacted result body, empty when opaque. resultHash string // matched is true only when identity, operation, path, guard, and the // configured result matcher all correlate. matched bool // mismatchReason explains why matched is false. mismatchReason string } // encodeWorkspaceCall produces a deterministic, safe payload for one operation // of the compiled binding from an issued tool call. It preserves typed // structured values, synthesizes deterministic shell-safe commands in command // mode, carries the public/provider identities, and emits a caller-executable // containment guard. It returns an error when the call does not match the bound // tool, the mapped path is missing, or the path fails lexical containment. func encodeWorkspaceCall(b *workspaceBinding, op workspaceOperationKind, call normalizedToolCall) (*workspaceEncodedPayload, error) { if b == nil { return nil, fmt.Errorf("nil binding") } ob := b.operation(op) if ob == nil { return nil, fmt.Errorf("binding has no %q operation", op) } if call.Arguments == nil { return nil, fmt.Errorf("nil call arguments") } if strings.TrimSpace(call.ID) == "" { return nil, fmt.Errorf("call is missing a public tool call id") } if strings.TrimSpace(call.Name) != ob.toolName { return nil, fmt.Errorf("call tool %q does not match bound tool %q for operation %q", call.Name, ob.toolName, op) } rawPath, ok := lookupMappedArgument(call.Arguments, ob.pathField) if !ok { return nil, fmt.Errorf("call is missing mapped path field %q", ob.pathField) } pathStr, ok := rawPath.(string) if !ok || strings.TrimSpace(pathStr) == "" { return nil, fmt.Errorf("mapped path field %q is not a non-empty string", ob.pathField) } safePath := lexicalNormalizePath(pathStr) if err := validateContainment(safePath); err != nil { return nil, err } payload := &workspaceEncodedPayload{ fingerprint: b.fingerprint, alternative: b.alternativeName, operation: op, mode: ob.mode, toolName: ob.toolName, publicCallID: strings.TrimSpace(call.ID), providerCallID: strings.TrimSpace(call.ProviderCallID), safePath: safePath, } switch ob.mode { case modeStructured: if err := encodeStructured(payload, ob, call, safePath); err != nil { return nil, err } case modeCommand: if err := encodeCommand(payload, ob, call, safePath); err != nil { return nil, err } default: return nil, fmt.Errorf("unknown binding mode %q", ob.mode) } payload.containmentGuard = synthesizeContainmentGuard(safePath, ob.createsParents) payload.correlationDigest = computePayloadCorrelationDigest(payload) if payload.correlationDigest == "" { return nil, fmt.Errorf("issued payload cannot be canonically correlated") } return payload, nil } // encodeStructured drives the outgoing argument map only from the compiled // argument map. The path is replaced with the containment-checked safe path; // content and mode values are carried through byte-for-byte with their original // types. No arbitrary extra fields are copied and no shell encoding is applied. func encodeStructured(payload *workspaceEncodedPayload, ob *workspaceOperationBinding, call normalizedToolCall, safePath string) error { args := make(map[string]any) setMappedArgument(args, ob.pathField, safePath) if ob.contentField != "" { if value, ok := lookupMappedArgument(call.Arguments, ob.contentField); ok { setMappedArgument(args, ob.contentField, cloneAnyValue(value)) } else if ob.op == opKindWrite { return fmt.Errorf("write call is missing mapped content field %q", ob.contentField) } } if ob.modeField != "" { if value, ok := lookupMappedArgument(call.Arguments, ob.modeField); ok { setMappedArgument(args, ob.modeField, cloneAnyValue(value)) } } payload.structuredArgs = args return nil } // encodeCommand synthesizes a deterministic command from the fixed argv // template. Placeholders {path} and {content} are substituted with the safe // path and the mapped content; every other token is a literal. Each argv // element is shell-safe single-quoted, so command output is stable regardless // of Go map iteration order and content bytes are preserved exactly. func encodeCommand(payload *workspaceEncodedPayload, ob *workspaceOperationBinding, call normalizedToolCall, safePath string) error { var content string if ob.contentField != "" { if value, ok := lookupMappedArgument(call.Arguments, ob.contentField); ok { content = commandArgumentString(value) } else if ob.op == opKindWrite { return fmt.Errorf("write call is missing mapped content field %q", ob.contentField) } } argv := make([]string, 0, len(ob.argvTemplate)) for _, token := range ob.argvTemplate { switch token { case "{path}": argv = append(argv, safePath) case "{content}": argv = append(argv, content) default: argv = append(argv, token) } } quoted := make([]string, len(argv)) for i, arg := range argv { quoted[i] = singleQuoteShell(arg) } payload.commandField = ob.commandField payload.commandArgv = argv payload.commandString = strings.Join(quoted, " ") payload.structuredArgs = map[string]any{ob.commandField: payload.commandString} return nil } // setMappedArgument assigns value at the dot-path key within args, creating // intermediate maps as needed. func setMappedArgument(args map[string]any, dotPath string, value any) { parts := strings.Split(dotPath, ".") current := args for i := 0; i < len(parts)-1; i++ { next, ok := current[parts[i]].(map[string]any) if !ok { next = make(map[string]any) current[parts[i]] = next } current = next } current[parts[len(parts)-1]] = value } // commandArgumentString renders a mapped value for command substitution. // Strings are used as-is; other JSON values are marshaled deterministically. func commandArgumentString(value any) string { if s, ok := value.(string); ok { return s } raw, err := json.Marshal(value) if err != nil { return "" } return string(raw) } // lexicalNormalizePath applies deterministic path normalization without // touching the filesystem: it trims, converts backslashes, collapses repeated // slashes, and resolves "." segments while preserving a leading slash so // validateContainment can reject absolute paths. ".." segments are preserved // so validateContainment can reject traversal. func lexicalNormalizePath(raw string) string { raw = strings.TrimSpace(raw) if raw == "" { return "" } isAbsolute := strings.HasPrefix(raw, "/") raw = strings.ReplaceAll(raw, "\\", "/") for strings.Contains(raw, "//") { raw = strings.ReplaceAll(raw, "//", "/") } parts := strings.Split(raw, "/") resolved := make([]string, 0, len(parts)) for i, part := range parts { if part == "." { continue } if i == 0 && part == "" && isAbsolute { resolved = append(resolved, "") continue } if part == "" { continue } resolved = append(resolved, part) } return strings.Join(resolved, "/") } // validateContainment lexically rejects paths that cannot be safely contained // in the workspace before any encoding: empty, over-long, absolute, traversal, // null-byte, and shell-metacharacter paths. func validateContainment(path string) error { if path == "" { return fmt.Errorf("empty path") } if len(path) > 4096 { return fmt.Errorf("path exceeds maximum length of 4096 characters") } if strings.HasPrefix(path, "/") { return fmt.Errorf("absolute path is not allowed: %q", path) } for _, segment := range strings.Split(path, "/") { if segment == ".." { return fmt.Errorf("path traversal is not allowed: %q", path) } } if strings.ContainsRune(path, 0) { return fmt.Errorf("path contains null byte") } for _, r := range path { switch { case r >= 'a' && r <= 'z': case r >= 'A' && r <= 'Z': case r >= '0' && r <= '9': case r == '.' || r == '-' || r == '_' || r == '/' || r == ' ': default: return fmt.Errorf("path contains unsafe character %q", string(r)) } } return nil } // synthesizeContainmentGuard returns a concrete caller-executed shell guard. // It resolves the canonical workspace cwd and either the existing target or a // canonical existing ancestor before the operation. Resolving the target itself // when it already exists is essential: checking only the parent would allow a // final-component symlink to escape the workspace. Parent-capable operations // may retain a validated nonexistent suffix after fencing their nearest existing // ancestor; operations without that capability still require the immediate // parent to exist. The Edge never evaluates this guard or accesses a workspace. func synthesizeContainmentGuard(relPath string, createsParents bool) string { quoted := singleQuoteShell(relPath) var b strings.Builder b.WriteString("{ ") b.WriteString(`IOP_WS_ROOT=$(realpath -e -- "${IOP_WORKSPACE_CWD:-.}") || exit 1; `) b.WriteString(`if [ "$IOP_WS_ROOT" = "/" ]; then IOP_WS_PREFIX=""; else IOP_WS_PREFIX="$IOP_WS_ROOT"; fi; `) b.WriteString(`IOP_WS_CANDIDATE="$IOP_WS_ROOT/`) b.WriteString(relPath) b.WriteString(`"; `) b.WriteString(`if [ -e "$IOP_WS_CANDIDATE" ] || [ -L "$IOP_WS_CANDIDATE" ]; then IOP_WS_TARGET=$(realpath -e -- "$IOP_WS_CANDIDATE") || exit 1; `) b.WriteString(`else `) if createsParents { b.WriteString(`IOP_WS_ANCESTOR="$IOP_WS_CANDIDATE"; IOP_WS_SUFFIX=""; `) b.WriteString(`while [ ! -e "$IOP_WS_ANCESTOR" ] && [ ! -L "$IOP_WS_ANCESTOR" ]; do IOP_WS_NAME=$(basename -- "$IOP_WS_ANCESTOR") || exit 1; `) b.WriteString(`if [ -n "$IOP_WS_SUFFIX" ]; then IOP_WS_SUFFIX="$IOP_WS_NAME/$IOP_WS_SUFFIX"; else IOP_WS_SUFFIX="$IOP_WS_NAME"; fi; `) b.WriteString(`IOP_WS_ANCESTOR=$(dirname -- "$IOP_WS_ANCESTOR") || exit 1; done; `) b.WriteString(`IOP_WS_ANCESTOR=$(realpath -e -- "$IOP_WS_ANCESTOR") || exit 1; `) b.WriteString(`IOP_WS_TARGET="$IOP_WS_ANCESTOR/$IOP_WS_SUFFIX"; `) } else { b.WriteString(`IOP_WS_PARENT=$(realpath -e -- "$(dirname -- "$IOP_WS_CANDIDATE")") || exit 1; `) b.WriteString(`IOP_WS_TARGET="$IOP_WS_PARENT/$(basename -- `) b.WriteString(quoted) b.WriteString(`)"; `) } b.WriteString(`fi; `) b.WriteString(`case "$IOP_WS_TARGET/" in "$IOP_WS_PREFIX"/*) : ;; *) echo 'iop: path escapes workspace root' >&2; exit 1 ;; esac; }`) return b.String() } // singleQuoteShell returns a POSIX single-quoted encoding of s. Bytes inside // single quotes are literal, so content is preserved exactly; embedded single // quotes are closed, escaped, and reopened. func singleQuoteShell(s string) string { return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'" } // matchResultReceipt correlates a caller-reported result against an issued // payload. A matched receipt requires the reported call id to equal the issued // public or provider id, the result body to parse, and the operation's // configured result matcher to match the normalized {status, result} envelope. // Opaque, error-shaped, wrong-id, and matcher-mismatched results do not match. func matchResultReceipt(b *workspaceBinding, payload *workspaceEncodedPayload, result workspaceResult) *workspaceResultReceipt { receipt := &workspaceResultReceipt{ operation: payload.operation, toolName: payload.toolName, path: payload.safePath, status: result.status, } if b != nil { receipt.fingerprint = b.fingerprint receipt.alternative = b.alternativeName } receipt.publicCallID = payload.publicCallID receipt.providerCallID = payload.providerCallID if len(result.body) > 0 { receipt.resultHash = sha256ResultHash(result.body) } if reason := matchResultCorrelation(b, payload, result); reason != "" { receipt.mismatchReason = reason return receipt } ob := b.operation(payload.operation) normalized, err := normalizeResultEnvelope(result) if err != nil { receipt.mismatchReason = "result body is not valid JSON" return receipt } if hasExplicitErrorSignal(normalized) { receipt.mismatchReason = "result contains an explicit error signal" return receipt } if !deepSubsetMatch(map[string]any(ob.resultMatcher), normalized) { receipt.mismatchReason = "result does not satisfy the configured result matcher" return receipt } receipt.matched = true return receipt } // matchResultCorrelation validates only immutable issue identity. Callers use // it to distinguish an exact caller-reported operation failure from malformed, // unknown, or untrusted continuation input before considering cleanup. func matchResultCorrelation(b *workspaceBinding, payload *workspaceEncodedPayload, result workspaceResult) string { if b == nil || payload == nil || b.fingerprint != payload.fingerprint { return "payload does not belong to binding" } if payload.correlationDigest == "" || payload.correlationDigest != computePayloadCorrelationDigest(payload) { return "issued payload correlation digest does not match" } if b.operation(payload.operation) == nil { return "binding has no such operation" } reportedID := strings.TrimSpace(result.callID) if reportedID == "" { return "result is missing a tool call id" } if reportedID != payload.publicCallID && reportedID != payload.providerCallID { return "result call id does not correlate with the issued call" } return "" } // workspaceResultIsExact reports whether a caller result carries a // self-describing operation report. An explicit failure status is exact on its // own; otherwise the non-empty body must decode into the normalized // {status, result} envelope. Opaque or malformed success bodies are untrusted // and stay fail-closed. func workspaceResultIsExact(result workspaceResult) bool { if hasExplicitErrorSignal(map[string]any{"status": result.status}) { return true } if len(bytes.TrimSpace(result.body)) == 0 { return false } _, err := normalizeResultEnvelope(result) return err == nil } // normalizeResultEnvelope builds the {status, result} envelope the configured // result matcher is evaluated against. An empty body yields a nil result, so an // opaque result cannot satisfy a matcher that requires result fields. func normalizeResultEnvelope(result workspaceResult) (map[string]any, error) { envelope := map[string]any{"status": result.status} if len(bytes.TrimSpace(result.body)) == 0 { envelope["result"] = nil return envelope, nil } var decoded any decoder := json.NewDecoder(bytes.NewReader(result.body)) decoder.UseNumber() if err := decoder.Decode(&decoded); err != nil { return nil, err } var trailing any if err := decoder.Decode(&trailing); err != io.EOF { if err == nil { return nil, fmt.Errorf("multiple JSON values are not allowed") } return nil, err } envelope["result"] = decoded return envelope, nil } // computePayloadCorrelationDigest binds every issued value that affects caller // execution or receipt admission. json.Marshal gives map keys a canonical // ordering, preserving typed values while avoiding Go map iteration variance. func computePayloadCorrelationDigest(payload *workspaceEncodedPayload) string { if payload == nil { return "" } description := map[string]any{ "fingerprint": payload.fingerprint, "alternative": payload.alternative, "operation": string(payload.operation), "mode": string(payload.mode), "tool_name": payload.toolName, "public_call_id": payload.publicCallID, "provider_call_id": payload.providerCallID, "safe_path": payload.safePath, "structured_args": payload.structuredArgs, "command_field": payload.commandField, "command_argv": payload.commandArgv, "command_string": payload.commandString, "containment_guard": payload.containmentGuard, } raw, err := json.Marshal(description) if err != nil { return "" } sum := sha256.Sum256(raw) return hex.EncodeToString(sum[:]) } // hasExplicitErrorSignal rejects success-shaped bodies that also declare an // endpoint error. It intentionally treats only semantically non-empty error // values as signals so optional null/false fields remain representable. func hasExplicitErrorSignal(value any) bool { switch v := value.(type) { case map[string]any: for key, child := range v { normalizedKey := strings.ToLower(strings.TrimSpace(key)) if (normalizedKey == "error" || normalizedKey == "errors") && errorValuePresent(child) { return true } if normalizedKey == "status" || normalizedKey == "type" { if text, ok := child.(string); ok { switch strings.ToLower(strings.TrimSpace(text)) { case "error", "failed", "failure": return true } } } if hasExplicitErrorSignal(child) { return true } } case []any: for _, child := range v { if hasExplicitErrorSignal(child) { return true } } } return false } func errorValuePresent(value any) bool { switch v := value.(type) { case nil: return false case bool: return v case string: return strings.TrimSpace(v) != "" case []any: return len(v) > 0 case map[string]any: return len(v) > 0 default: return true } } // sha256ResultHash computes a sha256 hex digest of the compacted result body // for stable, order-independent receipt hashing. func sha256ResultHash(body json.RawMessage) string { var buf bytes.Buffer if err := json.Compact(&buf, body); err != nil { buf.Reset() buf.Write(body) } sum := sha256.Sum256(buf.Bytes()) return hex.EncodeToString(sum[:]) }