Files
Agent-Native/adapters/native/contract_bind.go
T
weichen afbb567301 Bind session scope identity, enforce write sets, repair torn JSONL
Why: F02/F04/F15 - contract requests shared one transcript across
session scopes, Writable was dropped, and a torn JSONL tail poisoned
the next append; pure-text contract results lost their text.
Evidence: adapters/native, runtime/session, runtime/tools and
conformance tests pass (verify-u2.json); scope-isolation, write-set and
torn-tail regression cases added.
Impact: empty scope keeps legacy one-shot semantics; identity-less old
transcripts are never auto-adopted by a new scope.
Refs: feature/agent-stack-audit-remediation-b1 U2
2026-10-21 16:03:04 +00:00

92 lines
2.3 KiB
Go

package native
import (
"context"
"crypto/sha256"
"encoding/hex"
"path/filepath"
"strings"
"agent-native/exec"
)
// Contract-binding context keys: Writable / CapText / empty-target semantics
// stay inside the adapter so exec.Request remains unchanged for the legacy facade.
type ctxKey int
const (
keyWritable ctxKey = iota
keyAllowEmptyTargets
keyTextSink
)
func withWritable(ctx context.Context, paths []string) context.Context {
if len(paths) == 0 {
return ctx
}
return context.WithValue(ctx, keyWritable, append([]string(nil), paths...))
}
func writableFrom(ctx context.Context) []string {
v, _ := ctx.Value(keyWritable).([]string)
return v
}
func withAllowEmptyTargets(ctx context.Context) context.Context {
return context.WithValue(ctx, keyAllowEmptyTargets, true)
}
func allowEmptyTargets(ctx context.Context) bool {
v, _ := ctx.Value(keyAllowEmptyTargets).(bool)
return v
}
func withTextSink(ctx context.Context, sink *string) context.Context {
if sink == nil {
return ctx
}
return context.WithValue(ctx, keyTextSink, sink)
}
func captureText(ctx context.Context, text string) {
if sink, ok := ctx.Value(keyTextSink).(*string); ok && sink != nil {
*sink = text
}
}
// scopeIdentity returns a stable non-empty identity string for a non-zero
// SessionScope, or "" for the legacy one-shot / empty-scope path.
func scopeIdentity(s exec.SessionScope) string {
t := strings.TrimSpace(s.Tenant)
p := strings.TrimSpace(s.Project)
sess := strings.TrimSpace(s.Session)
if t == "" && p == "" && sess == "" {
return ""
}
return t + "/" + p + "/" + sess
}
// transcriptPathForScope binds storage identity to SessionScope.
// Empty scope keeps the historical fixed path under .agentx/transcript.jsonl.
func transcriptPathForScope(workDir, scopeID string) string {
if strings.TrimSpace(scopeID) == "" {
return filepath.Join(workDir, ".agentx", "transcript.jsonl")
}
sum := sha256.Sum256([]byte(scopeID))
return filepath.Join(workDir, ".agentx", "transcript."+hex.EncodeToString(sum[:16])+".jsonl")
}
func pathInWriteSet(allow []string, rel string) bool {
if len(allow) == 0 {
return true
}
rel = filepath.ToSlash(filepath.Clean(strings.TrimSpace(rel)))
for _, a := range allow {
if filepath.ToSlash(filepath.Clean(strings.TrimSpace(a))) == rel {
return true
}
}
return false
}