From 467c6018b822e39d8b21ee8c1addf8170a5af56c Mon Sep 17 00:00:00 2001 From: Rogee Date: Wed, 30 Sep 2026 12:39:21 +0800 Subject: [PATCH] Reject preserved legacy execution state on approved Agent startup --- .../saas-dispatcher-implementation.md | 1 + internal/rpc/approved_server.go | 7 +++++ internal/rpc/approved_server_test.go | 31 +++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/docs/evidence/saas-dispatcher-implementation.md b/docs/evidence/saas-dispatcher-implementation.md index a5881d2..7521eb0 100644 --- a/docs/evidence/saas-dispatcher-implementation.md +++ b/docs/evidence/saas-dispatcher-implementation.md @@ -113,6 +113,7 @@ - Agent 根命令不可达分支:删除旧执行模型的本地录音上传、手工重试、后台通知恢复和旧 Mock originate 代码及专属测试;根命令仍只注册获批的 Mock Agent/Dispatcher。当前录音 RPC、单次 PUT、失败保留和结果恢复另由现行隔离测试覆盖,删除旧命令不改变现有业务数据。 - Agent RPC 客户端入口:删除没有现行调用者的旧 `Client`/`DialFromFiles` 包装及仅针对该包装的测试;当前命令仍通过部署提供的双向 TLS 配置建立 gRPC 连接,原证书信任、SAN 和握手回归测试保持不变。旧服务端业务 RPC 与 Proto 仍待后续清理。 - Agent 会话持久边界:将会话代际使用的私有原子落盘函数从旧执行日志模块移入独立会话日志文件;原有目录同步、写失败报错和重启代际栅栏测试继续通过。旧执行日志及业务 RPC 尚未删除;此次只移动源码,不修改现存 Agent 恢复文件。 +- Agent 旧执行状态屏障:批准的 Mock Agent 启动时若发现旧执行日志,立即拒绝启动并明确要求人工处置;隔离测试先复现了原先允许启动的问题,再证明旧日志字节不变、新会话状态未写入。此屏障不迁移、不清理存量文件;没有检查或处置真实环境中的旧记录。 ## 验收台账 diff --git a/internal/rpc/approved_server.go b/internal/rpc/approved_server.go index 0b201cd..1edad14 100644 --- a/internal/rpc/approved_server.go +++ b/internal/rpc/approved_server.go @@ -2,6 +2,8 @@ package rpc import ( "errors" + "fmt" + "os" "strings" ) @@ -21,6 +23,11 @@ func NewApprovedAgentServer(options ServerOptions, worker *ApprovedCallWorker) ( if worker.Lifecycle == nil || worker.Calls == nil || worker.Prepare == nil || worker.OnFailure == nil { return nil, errors.New("approved Agent requires a process lifecycle, task calls, runner and failure reporting") } + if _, err := os.Lstat(options.StatePath + ".executions"); err == nil { + return nil, errors.New("legacy Agent execution state requires operator disposition") + } else if !errors.Is(err, os.ErrNotExist) { + return nil, fmt.Errorf("inspect legacy Agent execution state: %w", err) + } if options.MockApprovedOriginate != nil || options.ApprovedTaskCalls != nil { return nil, errors.New("approved Agent cannot use competing call or control adapters") } diff --git a/internal/rpc/approved_server_test.go b/internal/rpc/approved_server_test.go index 6420f95..2e6e5f4 100644 --- a/internal/rpc/approved_server_test.go +++ b/internal/rpc/approved_server_test.go @@ -145,6 +145,37 @@ func TestApprovedAgentServerRefusesMissingOrCompetingAdapters(t *testing.T) { } } +func TestApprovedAgentServerRefusesLegacyExecutionJournalWithoutMutation(t *testing.T) { + state := filepath.Join(t.TempDir(), "agent-session.json") + legacy := state + ".executions" + original := []byte(`{"executions":{}}`) + if err := os.WriteFile(legacy, original, 0600); err != nil { + t.Fatal(err) + } + worker := &ApprovedCallWorker{Lifecycle: context.Background(), Calls: &agent.TaskCalls{}, + Prepare: prepareWorkerRun(func(context.Context, ApprovedExecution) error { return nil }), + OnFailure: func(ApprovedExecution, error) error { return nil }, + } + req := approvedTestRequest(t, time.Date(2026, 9, 20, 10, 0, 0, 0, time.UTC)) + server, err := NewApprovedAgentServer(ServerOptions{ + Mode: "mock", StatePath: state, ApprovedDispatcherID: req.DispatcherId, + Status: &agentpb.AgentStatus{AgentId: "agent-1", CellId: "cell-1", BootId: "boot-1"}, + LoadedSIP: func(context.Context) (map[string]int64, error) { + return map[string]int64{req.SelectedTrunkId: req.SipRevision}, nil + }, + }, worker) + if err == nil || server != nil { + t.Fatalf("legacy execution journal must block approved Agent startup: %v", err) + } + retained, err := os.ReadFile(legacy) + if err != nil || string(retained) != string(original) { + t.Fatalf("legacy execution journal was changed: %v", err) + } + if _, err := os.Stat(state); !os.IsNotExist(err) { + t.Fatalf("new session state was written before legacy data review: %v", err) + } +} + func TestApprovedAgentServerRejectsSelfReportedDispatcherBeforeSessionWrite(t *testing.T) { now := time.Date(2026, 9, 20, 10, 0, 0, 0, time.UTC) req := approvedTestRequest(t, now)