202 lines
8.6 KiB
Go
202 lines
8.6 KiB
Go
// call-memory-admin is an operator-only maintenance command. It requires direct
|
|||
|
|
// database access; evidence references are audit attestations, not test results.
|
||
|
|
package main
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"data-terminal-cloud/internal/callmemory"
|
||
|
|
"encoding/json"
|
||
|
|
"flag"
|
||
|
|
"fmt"
|
||
|
|
"github.com/google/uuid"
|
||
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
||
|
|
"os"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
func main() {
|
||
|
|
if e := run(); e != nil {
|
||
|
|
fmt.Fprintln(os.Stderr, "call-memory-admin:", e)
|
||
|
|
os.Exit(1)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
type profileFile struct {
|
||
|
|
ID string `json:"id"`
|
||
|
|
SDK string `json:"sdk_version"`
|
||
|
|
Bot string `json:"bot_id"`
|
||
|
|
Model string `json:"model_version"`
|
||
|
|
Transport string `json:"transport"`
|
||
|
|
Audio map[string]any `json:"audio_profile"`
|
||
|
|
Template map[string]any `json:"vendor_template"`
|
||
|
|
Mapping map[string]string `json:"field_mapping"`
|
||
|
|
Capabilities map[string]bool `json:"capabilities"`
|
||
|
|
Evidence map[string]string `json:"evidence"`
|
||
|
|
Schema json.RawMessage `json:"projection_schema"`
|
||
|
|
}
|
||
|
|
|
||
|
|
func run() error {
|
||
|
|
op := flag.String("operation", "", "import-profile, attest-call-stopped, attest-legacy-cleanup, attest-backup-cleanup, export-deletion-ledger, replay-deletion-ledger, retry-projection")
|
||
|
|
file := flag.String("file", "", "input JSON file (export writes to stdout)")
|
||
|
|
subject := flag.String("subject-id", "", "internal subject UUID")
|
||
|
|
session := flag.String("session-id", "", "business session UUID for attest-call-stopped")
|
||
|
|
evidence := flag.String("evidence", "", "immutable audit record reference")
|
||
|
|
publish := flag.Bool("publish", false, "publish profile after capability validation")
|
||
|
|
flag.Parse()
|
||
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||
|
|
defer cancel()
|
||
|
|
p, e := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
|
||
|
|
if e != nil {
|
||
|
|
return fmt.Errorf("database configuration invalid")
|
||
|
|
}
|
||
|
|
defer p.Close()
|
||
|
|
bytes := func(v any) []byte { b, _ := json.Marshal(v); return b }
|
||
|
|
switch *op {
|
||
|
|
case "attest-call-stopped":
|
||
|
|
if _, e = uuid.Parse(*session); e != nil || strings.TrimSpace(*evidence) == "" {
|
||
|
|
return fmt.Errorf("session-id and evidence are required")
|
||
|
|
}
|
||
|
|
tx, e := p.Begin(ctx)
|
||
|
|
if e != nil {
|
||
|
|
return e
|
||
|
|
}
|
||
|
|
defer tx.Rollback(context.Background())
|
||
|
|
var sid string
|
||
|
|
var localOff, sdkStopped bool
|
||
|
|
if e = tx.QueryRow(ctx, `SELECT subject_id::text,local_audio_off,sdk_stopped FROM call_session WHERE id=$1::uuid FOR UPDATE`, *session).Scan(&sid, &localOff, &sdkStopped); e != nil {
|
||
|
|
return e
|
||
|
|
}
|
||
|
|
if !localOff || !sdkStopped {
|
||
|
|
return fmt.Errorf("device must first prove local audio off and SDK stopped")
|
||
|
|
}
|
||
|
|
if _, e = tx.Exec(ctx, `UPDATE call_session SET remote_state='STOPPED_OBSERVED' WHERE id=$1::uuid`, *session); e != nil {
|
||
|
|
return e
|
||
|
|
}
|
||
|
|
if _, e = tx.Exec(ctx, `INSERT INTO call_memory_operator_evidence(subject_id,call_session_id,operation,evidence_ref) VALUES($1::uuid,$2::uuid,'attest-call-stopped',$3)`, sid, *session, *evidence); e != nil {
|
||
|
|
return e
|
||
|
|
}
|
||
|
|
return tx.Commit(ctx)
|
||
|
|
case "import-profile":
|
||
|
|
raw, e := os.ReadFile(*file)
|
||
|
|
if e != nil {
|
||
|
|
return e
|
||
|
|
}
|
||
|
|
var in profileFile
|
||
|
|
if e = callmemory.StrictJSON(raw, &in); e != nil {
|
||
|
|
return e
|
||
|
|
}
|
||
|
|
profile := callmemory.Profile{ID: in.ID, SDKVersion: in.SDK, BotID: in.Bot, ModelVersion: in.Model, Transport: in.Transport, Audio: in.Audio, Template: in.Template, Mapping: in.Mapping, Capabilities: in.Capabilities, Evidence: in.Evidence}
|
||
|
|
if *publish {
|
||
|
|
if _, e = profile.Compile("OFF", "", "", "", "", "test"); e != nil {
|
||
|
|
return e
|
||
|
|
}
|
||
|
|
if profile.Capabilities["H03"] || profile.Capabilities["H12"] {
|
||
|
|
if e = profile.Check("NATIVE_VIKING"); e != nil {
|
||
|
|
return e
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if len(in.Schema) == 0 {
|
||
|
|
in.Schema = json.RawMessage(`{}`)
|
||
|
|
}
|
||
|
|
_, e = p.Exec(ctx, `INSERT INTO call_provider_profile(id,protocol_version,sdk_version,bot_id,model_version,transport,audio_profile,vendor_template,field_mapping,capabilities,evidence,projection_schema,published) VALUES($1,'CALL-MEMORY-1.1',$2,$3,$4,$5,$6::jsonb,$7::jsonb,$8::jsonb,$9::jsonb,$10::jsonb,$11::jsonb,$12)`, in.ID, in.SDK, in.Bot, in.Model, in.Transport, bytes(in.Audio), bytes(in.Template), bytes(in.Mapping), bytes(in.Capabilities), bytes(in.Evidence), in.Schema, *publish)
|
||
|
|
if e != nil {
|
||
|
|
return fmt.Errorf("profile insert failed (existing published profiles are immutable)")
|
||
|
|
}
|
||
|
|
case "attest-legacy-cleanup", "attest-backup-cleanup", "retry-projection":
|
||
|
|
if _, e = uuid.Parse(*subject); e != nil || strings.TrimSpace(*evidence) == "" {
|
||
|
|
return fmt.Errorf("subject-id and evidence are required")
|
||
|
|
}
|
||
|
|
tx, e := p.Begin(ctx)
|
||
|
|
if e != nil {
|
||
|
|
return e
|
||
|
|
}
|
||
|
|
defer tx.Rollback(context.Background())
|
||
|
|
if *op == "attest-legacy-cleanup" {
|
||
|
|
// Requires proof of old namespace cleanup and no unresolved old runtime.
|
||
|
|
var active bool
|
||
|
|
if e = tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM conversation WHERE subject_id=$1::uuid AND runtime_type='volc_hardware_agent' AND status NOT IN ('COMPLETED','FAILED'))`, *subject).Scan(&active); e != nil {
|
||
|
|
return e
|
||
|
|
}
|
||
|
|
if active {
|
||
|
|
return fmt.Errorf("legacy call still unresolved")
|
||
|
|
}
|
||
|
|
_, e = tx.Exec(ctx, `UPDATE memory_user_state SET legacy_projection_pending=false,updated_at=now() WHERE subject_id=$1::uuid`, *subject)
|
||
|
|
} else if *op == "attest-backup-cleanup" {
|
||
|
|
_, e = tx.Exec(ctx, `UPDATE memory_deletion_ledger SET backup_cleanup_verified_at=now(),evidence_ref=$2 WHERE subject_id=$1::uuid AND backup_cleanup_verified_at IS NULL`, *subject, *evidence)
|
||
|
|
} else {
|
||
|
|
_, e = tx.Exec(ctx, `UPDATE memory_projection_job SET state='RECONCILING',last_error=NULL,next_attempt_at=now() WHERE subject_id=$1::uuid AND state='BLOCKED'`, *subject)
|
||
|
|
}
|
||
|
|
if e != nil {
|
||
|
|
return e
|
||
|
|
}
|
||
|
|
_, e = tx.Exec(ctx, `INSERT INTO call_memory_operator_evidence(subject_id,operation,evidence_ref) VALUES($1::uuid,$2,$3)`, *subject, *op, *evidence)
|
||
|
|
if e != nil {
|
||
|
|
return e
|
||
|
|
}
|
||
|
|
if e = tx.Commit(ctx); e != nil {
|
||
|
|
return e
|
||
|
|
}
|
||
|
|
case "export-deletion-ledger":
|
||
|
|
var data []byte
|
||
|
|
e = p.QueryRow(ctx, `SELECT COALESCE(jsonb_agg(jsonb_build_object('memory_id',memory_id,'subject_id',subject_id,'privacy_epoch',privacy_epoch,'scope',scope)),'[]'::jsonb) FROM memory_deletion_ledger`).Scan(&data)
|
||
|
|
if e != nil {
|
||
|
|
return e
|
||
|
|
}
|
||
|
|
fmt.Println(string(data))
|
||
|
|
return nil
|
||
|
|
case "replay-deletion-ledger":
|
||
|
|
raw, e := os.ReadFile(*file)
|
||
|
|
if e != nil {
|
||
|
|
return e
|
||
|
|
}
|
||
|
|
var entries []struct {
|
||
|
|
Memory string `json:"memory_id"`
|
||
|
|
Subject string `json:"subject_id"`
|
||
|
|
Epoch int64 `json:"privacy_epoch"`
|
||
|
|
Scope string `json:"scope"`
|
||
|
|
}
|
||
|
|
if e = callmemory.StrictJSON(raw, &entries); e != nil {
|
||
|
|
return e
|
||
|
|
}
|
||
|
|
tx, e := p.Begin(ctx)
|
||
|
|
if e != nil {
|
||
|
|
return e
|
||
|
|
}
|
||
|
|
defer tx.Rollback(context.Background())
|
||
|
|
for _, r := range entries {
|
||
|
|
if _, e = uuid.Parse(r.Memory); e != nil {
|
||
|
|
return e
|
||
|
|
}
|
||
|
|
if _, e = uuid.Parse(r.Subject); e != nil {
|
||
|
|
return e
|
||
|
|
}
|
||
|
|
if r.Epoch < 0 || (r.Scope != "MEMORY_AND_DERIVED" && r.Scope != "SOURCE_AND_DERIVED") {
|
||
|
|
return fmt.Errorf("invalid ledger")
|
||
|
|
}
|
||
|
|
// Restore only matching records. The normal deletion pipeline creates fresh
|
||
|
|
// projection work and revokes calls; external service stays offline until done.
|
||
|
|
_, e = tx.Exec(ctx, `UPDATE curated_memory SET status='PENDING_DELETE',proactive_policy='NEVER',version=version+1 WHERE id=$1::uuid AND subject_id=$2::uuid AND status NOT IN('PENDING_DELETE','DELETED')`, r.Memory, r.Subject)
|
||
|
|
if e != nil {
|
||
|
|
return e
|
||
|
|
}
|
||
|
|
_, e = tx.Exec(ctx, `INSERT INTO memory_deletion_ledger(memory_id,privacy_epoch,subject_id,scope) SELECT id,$3,subject_id,$4 FROM curated_memory WHERE id=$1::uuid AND subject_id=$2::uuid ON CONFLICT(memory_id,privacy_epoch) DO UPDATE SET scope=CASE WHEN excluded.scope='SOURCE_AND_DERIVED' THEN excluded.scope ELSE memory_deletion_ledger.scope END,backup_cleanup_verified_at=NULL`, r.Memory, r.Subject, r.Epoch, r.Scope)
|
||
|
|
if e != nil {
|
||
|
|
return e
|
||
|
|
}
|
||
|
|
_, e = tx.Exec(ctx, `INSERT INTO memory_mutation(id,tenant_id,subject_id,action,request_digest,state,targets,scope,memory_revision,privacy_epoch) SELECT gen_random_uuid(),cm.tenant_id,cm.subject_id,'DELETE','restore-ledger','PROPAGATING',jsonb_build_array(cm.public_id::text),$3,us.memory_revision,us.privacy_epoch FROM curated_memory cm JOIN memory_user_state us ON us.subject_id=cm.subject_id WHERE cm.id=$1::uuid AND cm.subject_id=$2::uuid AND NOT EXISTS(SELECT 1 FROM memory_mutation mm WHERE mm.subject_id=cm.subject_id AND mm.action='DELETE' AND mm.targets @> jsonb_build_array(cm.public_id::text) AND mm.state IN('PROPAGATING','BLOCKED'))`, r.Memory, r.Subject, r.Scope)
|
||
|
|
if e != nil {
|
||
|
|
return e
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if e = tx.Commit(ctx); e != nil {
|
||
|
|
return e
|
||
|
|
}
|
||
|
|
default:
|
||
|
|
return fmt.Errorf("unknown operation; use -help")
|
||
|
|
}
|
||
|
|
fmt.Println("operation recorded")
|
||
|
|
return nil
|
||
|
|
}
|