325 lines
10 KiB
Go
325 lines
10 KiB
Go
// 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")
|
||
|
|
}
|