ci / verify (push) Canceled after 0s
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
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")
|
|
}
|