Files

325 lines
10 KiB
Go
Raw Permalink Normal View History

// Package callmemory implements CALL-MEMORY-1.1. Provider capabilities are
// evidence-backed release gates, never inferred from a successful configuration write.
package callmemory
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"sort"
"strings"
"time"
"unicode/utf8"
)
const ContractVersion = "CALL-MEMORY-1.1"
const MaxBody = 8 << 10
const MaxMemoryBody = 64 << 10
const LeaseMS int64 = 45000
const ManifestMS int64 = 30000
type Error struct {
Status int
Code, Message, Recovery string
}
func (e *Error) Error() string { return e.Code }
func fault(status int, code string) *Error { return &Error{status, code, code, "NONE"} }
func hash(b []byte) string { h := sha256.Sum256(b); return hex.EncodeToString(h[:]) }
func jsonBytes(v any) []byte { b, _ := json.Marshal(v); return b }
// StrictJSON rejects ambiguous duplicate keys, trailing values and unknown struct fields.
func StrictJSON(b []byte, v any) error {
if !utf8.Valid(b) {
return fault(400, "INVALID_JSON")
}
d := json.NewDecoder(bytes.NewReader(b))
d.UseNumber()
var walk func() error
walk = func() error {
t, e := d.Token()
if e != nil {
return e
}
delim, ok := t.(json.Delim)
if !ok {
return nil
}
switch delim {
case '{':
keys := map[string]bool{}
for d.More() {
k, e := d.Token()
if e != nil {
return e
}
s, ok := k.(string)
if !ok || keys[s] {
return errors.New("duplicate key")
}
keys[s] = true
if e = walk(); e != nil {
return e
}
}
case '[':
for d.More() {
if e := walk(); e != nil {
return e
}
}
default:
return errors.New("invalid delimiter")
}
_, e = d.Token()
return e
}
if e := walk(); e != nil {
return fault(400, "INVALID_JSON")
}
if _, e := d.Token(); e != io.EOF {
return fault(400, "INVALID_JSON")
}
d = json.NewDecoder(bytes.NewReader(b))
d.DisallowUnknownFields()
if e := d.Decode(v); e != nil {
return fault(400, "INVALID_JSON")
}
return nil
}
func ManifestMAC(key []byte, b64 string) string {
h := hmac.New(sha256.New, key)
h.Write([]byte("CALLCFG1\n" + b64))
return hex.EncodeToString(h.Sum(nil))
}
func EncodeManifest(key []byte, m Manifest) (string, string, error) {
b, e := json.Marshal(m)
if e != nil || len(key) != 32 || len(b) > 24<<10 {
return "", "", fault(400, "INVALID_MANIFEST")
}
encoded := base64.RawURLEncoding.EncodeToString(b)
return encoded, ManifestMAC(key, encoded), nil
}
func DecodeManifest(key []byte, b64, mac string) (Manifest, error) {
var m Manifest
if len(key) != 32 || len(b64) > 32<<10 || len(mac) != 64 || !hmac.Equal([]byte(ManifestMAC(key, b64)), []byte(mac)) {
return m, fault(400, "MANIFEST_INVALID")
}
b, e := base64.RawURLEncoding.DecodeString(b64)
if e != nil || base64.RawURLEncoding.EncodeToString(b) != b64 {
return m, fault(400, "MANIFEST_INVALID")
}
e = StrictJSON(b, &m)
if e == nil {
e = m.Validate()
}
return m, e
}
// Validate is also used by device clients after authenticating the original bytes.
func (m Manifest) Validate() error {
if m.ContractVersion != ContractVersion || !validUUID(m.ManifestID) || !validUUID(m.SessionID) || !short(m.DeviceID) || !validUUID(m.BootID) || !short(m.HookID) || m.CallGeneration < 0 || m.BindingGeneration < 0 || m.Mode != "RTC" || !short(m.BotID) || !short(m.ProfileID) || !short(m.AudioProfileID) || !oneOf(m.Purpose, "NORMAL", "FIRST_TEST") || !oneOf(m.FirstTurnMode, "AI_WELCOME", "USER_FIRST") || !oneOf(m.MemoryMode, "OFF", "HOT_ONLY", "NATIVE_VIKING") || m.MemoryRevision < 0 || m.VisibleRevision < 0 || m.VisibleRevision > m.MemoryRevision || m.PrivacyEpoch < 0 || m.ContextVersion < 0 || m.IssuedAt <= 0 || m.ExpiresAt <= m.IssuedAt || m.ExpiresAt-m.IssuedAt > ManifestMS || m.LeaseMS <= 0 || m.LeaseMS > LeaseMS || m.LeaseExpiresAt <= m.IssuedAt || m.LeaseExpiresAt-m.IssuedAt > m.LeaseMS || m.MaxCallMS <= 0 || m.MaxCallMS > 3600000 || len(m.VendorParams) > 16<<10 || !json.Valid([]byte(m.VendorParams)) || (m.LocalNotice && !short(m.NoticeProfileID)) {
return fault(400, "MANIFEST_INVALID")
}
return nil
}
type Manifest struct {
ContractVersion string `json:"contract_version"`
ManifestID string `json:"manifest_id"`
SessionID string `json:"business_session_id"`
DeviceID string `json:"device_id"`
BootID string `json:"boot_id"`
HookID string `json:"hook_event_id"`
CallGeneration int64 `json:"call_generation"`
BindingGeneration int64 `json:"binding_generation"`
IssuedAt int64 `json:"issued_at_ms"`
ExpiresAt int64 `json:"expires_at_ms"`
Mode string `json:"mode"`
BotID string `json:"bot_id"`
ProfileID string `json:"provider_profile_id"`
Purpose string `json:"purpose"`
FirstTurnMode string `json:"first_turn_mode"`
MemoryMode string `json:"memory_mode"`
MemoryRevision int64 `json:"memory_revision"`
VisibleRevision int64 `json:"viking_visible_revision"`
PrivacyEpoch int64 `json:"privacy_epoch"`
ContextVersion int64 `json:"context_version"`
AudioProfileID string `json:"audio_profile_id"`
VendorParams string `json:"vendor_params_json"`
LeaseMS int64 `json:"auth_lease_ms"`
LeaseExpiresAt int64 `json:"auth_lease_expires_at_ms"`
MaxCallMS int64 `json:"max_call_ms"`
LocalNotice bool `json:"local_notice_enabled"`
NoticeProfileID string `json:"notice_profile_id"`
}
type Profile struct {
ID, SDKVersion, BotID, ModelVersion, Transport string
Audio map[string]any
Template map[string]any
Mapping map[string]string
Capabilities map[string]bool
Evidence map[string]string
}
func (p Profile) Check(mode string) error {
if !oneOf(mode, "OFF", "HOT_ONLY", "NATIVE_VIKING") || p.ID == "" || p.SDKVersion == "" || p.BotID == "" || p.ModelVersion == "" || p.Transport != "RTC" {
return fault(409, "PROVIDER_PROFILE_UNVERIFIED")
}
for _, g := range []string{"H01", "H02", "H04", "H05", "H06", "H07", "H09", "H11"} {
if strings.TrimSpace(p.Evidence[g]) == "" || !p.Capabilities[g] {
return fault(409, "PROVIDER_PROFILE_UNVERIFIED")
}
}
if mode == "NATIVE_VIKING" {
for _, g := range []string{"H03", "H08", "H12"} {
if p.Evidence[g] == "" || !p.Capabilities[g] {
return fault(409, "PROVIDER_PROFILE_UNVERIFIED")
}
}
}
return nil
}
// Compile changes only explicitly reviewed JSON pointer paths. OFF clears the
// exact same fields used by native memory; no guessed provider field names.
func (p Profile) Compile(mode, context, welcome, user, assistant, dialog string) (string, error) {
if e := p.Check(mode); e != nil {
return "", e
}
var out map[string]any
if e := json.Unmarshal(jsonBytes(p.Template), &out); e != nil {
return "", e
}
paths := []string{}
for _, name := range []string{"memory_enabled", "hot_context", "welcome_text", "user_id", "assistant_id", "dialog_id"} {
path := p.Mapping[name]
for _, prior := range paths {
if path == prior || strings.HasPrefix(path, prior+"/") || strings.HasPrefix(prior, path+"/") {
return "", fault(409, "PROVIDER_MAPPING_UNVERIFIED")
}
}
paths = append(paths, path)
}
values := map[string]any{"memory_enabled": mode == "NATIVE_VIKING", "hot_context": context, "welcome_text": welcome, "user_id": user, "assistant_id": assistant, "dialog_id": dialog}
if mode == "OFF" {
values["hot_context"] = ""
values["welcome_text"] = ""
}
if mode != "NATIVE_VIKING" {
values["user_id"] = ""
values["assistant_id"] = ""
}
for name, v := range values {
path := p.Mapping[name]
if path == "" {
return "", fault(409, "PROVIDER_MAPPING_UNVERIFIED")
}
if e := setPointer(out, path, v); e != nil {
return "", e
}
}
b, e := json.Marshal(out)
if e != nil || len(b) > 16<<10 {
return "", fault(413, "VENDOR_PARAMS_TOO_LARGE")
}
return string(b), nil
}
func setPointer(m map[string]any, path string, value any) error {
if !strings.HasPrefix(path, "/") {
return fault(409, "PROVIDER_MAPPING_UNVERIFIED")
}
parts := strings.Split(path[1:], "/")
for i, raw := range parts {
k := strings.ReplaceAll(strings.ReplaceAll(raw, "~1", "/"), "~0", "~")
if k == "" {
return fault(409, "PROVIDER_MAPPING_UNVERIFIED")
}
if i == len(parts)-1 {
m[k] = value
return nil
}
n, ok := m[k].(map[string]any)
if !ok {
return fault(409, "PROVIDER_MAPPING_UNVERIFIED")
}
m = n
}
return nil
}
type ContextFact struct {
ID, EntityKey, Text, Type, ItemStatus, Policy string
Version int64
SourceTime time.Time
}
func BuildContext(facts []ContextFact, limit int) string {
// Only the latest valid record for one entity participates. Keep complete facts
// so truncation cannot strip a negation, a date, or an uncertainty qualifier.
sort.SliceStable(facts, func(i, j int) bool {
if !facts[i].SourceTime.Equal(facts[j].SourceTime) {
return facts[i].SourceTime.After(facts[j].SourceTime)
}
if facts[i].Version != facts[j].Version {
return facts[i].Version > facts[j].Version
}
return facts[i].ID < facts[j].ID
})
seen := map[string]bool{}
parts := []string{}
used := 0
for _, f := range facts {
key := f.EntityKey
if key == "" {
key = f.ID
}
if seen[key] {
continue
}
seen[key] = true
if f.Policy != "ALLOWED" || strings.TrimSpace(f.Text) == "" {
continue
}
if f.Type == "OPEN_ITEM" && f.ItemStatus != "OPEN" {
continue
}
text := strings.TrimSpace(f.Text)
n := utf8.RuneCountInString(text)
if used+n+1 > limit {
continue
}
used += n + 1
parts = append(parts, text)
}
return strings.Join(parts, "\n")
}
func SelectMemoryMode(revision, visible int64, critical, consent, offVerified bool) (string, error) {
if !consent {
if !offVerified {
return "", fault(409, "MEMORY_OFF_UNVERIFIED")
}
return "OFF", nil
}
if critical {
if !offVerified {
return "", fault(409, "MEMORY_CRITICAL_LAG")
}
return "HOT_ONLY", nil
}
if revision == 0 {
return "HOT_ONLY", nil
}
if visible <= revision {
return "NATIVE_VIKING", nil
}
return "", fmt.Errorf("invalid memory watermark")
}