fix(agent): refuse legacy spool before starting approved recovery
This commit is contained in:
@@ -6,11 +6,13 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"maps"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -50,6 +52,9 @@ func newCurrentAgentServer(ctx context.Context, settings config.AgentEnvironment
|
||||
if err != nil || !root.IsDir() || root.Mode().Perm() != 0700 {
|
||||
return nil, errors.New("current Agent requires an existing private 0700 recovery directory")
|
||||
}
|
||||
if err := rejectLegacyAgentSpool(settings.RecoveryRoot); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for trunk, revision := range appliedSIP {
|
||||
if strings.TrimSpace(trunk) == "" || revision <= 0 {
|
||||
return nil, errors.New("current Agent requires explicit applied Mock SIP revisions")
|
||||
@@ -116,6 +121,32 @@ func newCurrentAgentServer(ctx context.Context, settings config.AgentEnvironment
|
||||
return handler, nil
|
||||
}
|
||||
|
||||
// Old spool files may contain unreported execution or upload outcomes. Never
|
||||
// start the current Agent on the same root without an explicit disposition.
|
||||
func rejectLegacyAgentSpool(root string) error {
|
||||
entries, err := os.ReadDir(root)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inspect Agent recovery root: %w", err)
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.Name() == ".uploads" || entry.Name() == ".upload-locks" {
|
||||
return errors.New("legacy Agent upload spool requires manual disposition")
|
||||
}
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
info, err := os.Lstat(filepath.Join(root, entry.Name(), "state.json"))
|
||||
if err == nil {
|
||||
if !info.IsDir() {
|
||||
return errors.New("legacy Agent execution spool requires manual disposition")
|
||||
}
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("inspect Agent recovery root: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// The isolated Mock uploader may reach localhost only, even if a grant or
|
||||
// redirect unexpectedly names a real OSS endpoint. It never logs signed URLs.
|
||||
func localMockHTTPClient(trustPEM []byte) (*http.Client, error) {
|
||||
|
||||
@@ -58,6 +58,45 @@ func TestNewCurrentAgentServerBindsMockCallsToOneSessionAndRecoveryRoot(t *testi
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCurrentAgentServerRefusesLegacySpoolWithoutChangingIt(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
marker string
|
||||
dir bool
|
||||
}{
|
||||
{name: "old upload attempts", marker: ".uploads", dir: true},
|
||||
{name: "old upload locks", marker: ".upload-locks", dir: true},
|
||||
{name: "old execution state", marker: filepath.Join("old-execution", "state.json")},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
settings, scenario := currentAgentSetupFixture(t)
|
||||
marker := filepath.Join(settings.RecoveryRoot, tc.marker)
|
||||
if tc.dir {
|
||||
if err := os.Mkdir(marker, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
if err := os.Mkdir(filepath.Dir(marker), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(marker, []byte(`{"schema_version":"1"}`), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
server, err := newCurrentAgentServer(context.Background(), settings, scenario, map[string]int64{"trunk-mock": 8}, &isolatedAgentRecordingClient{})
|
||||
if err == nil || server != nil {
|
||||
t.Fatalf("legacy spool was admitted: %v", err)
|
||||
}
|
||||
if _, err := os.Lstat(marker); err != nil {
|
||||
t.Fatalf("legacy spool changed: %v", err)
|
||||
}
|
||||
if _, err := os.Lstat(settings.SessionPath); !os.IsNotExist(err) {
|
||||
t.Fatalf("rejected Agent created session state: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCurrentAgentServerRefusesUnsafeAdaptersBeforeResources(t *testing.T) {
|
||||
settings, scenario := currentAgentSetupFixture(t)
|
||||
for _, tc := range []struct {
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
- Agent 旧业务 RPC 处理器:先以服务结构测试复现旧 `GetBootstrap` 等十个方法仍存在,再删除旧执行、许可、控制、查询、事件和上传处理器及仅依赖旧服务面的专属测试;原混合测试保留会话代际、证书指纹、状态和真实 gRPC 激活。另将旧执行日志写入失败保护迁至现行获批执行测试:日志目录不可写时两次相同请求均不得接受或发起呼叫,修复目录后只发起一次,结果未知时拒绝重拨。`go test ./...`、`go test -race ./...`、`go vet ./...`、`go build ./...`、Proto 来源/hash、当前合同及临时 RabbitMQ/HTTPS/双向 TLS 隔离链路均通过;旧执行日志结构、未使用的 Proto 消息与其他引用仍须继续清理,未接触现存业务数据或真实外部服务。
|
||||
- Agent 旧执行日志根因:新增「首次激活→同一路径重启」测试,复现现行入口曾在初次启动自动写出废弃的 `.executions` 文件,下一次启动又将它识别为旧未交付状态并拒绝服务。移除旧日志写入、回放状态和闲置执行配置,只保留当前 `.approved` 日志、会话代际和旧文件存在时拒绝启动的保护;回归确认首次启动与重启不会产生新旧执行日志。已有 `.executions` 一律保留并失败关闭,不自动清理或猜测其业务内容;当前测试只使用临时目录。
|
||||
- Agent 旧静态制品入口:现行命令从未提供 `StaticArtifactRaw/Expected`,旧激活分支及只服务于旧 `static-cell-artifact-v0.2` Schema 的手写解析器无法证明 Asterisk 实际加载。结构测试先复现残留,再移除旧 RPC 参数、解析器与专属测试;保留当前隔离 Mock 的 `LoadedSIP` revision 回报及 Dispatcher SIP 版本准入校验。此变更不等于真实 Agent/Asterisk 已加载或管理平台已审批,历史 Schema/来源事实另行辨析。
|
||||
- Agent 旧 spool 准入边界:隔离测试先复现现行 Agent 对同一恢复根目录中旧 `.uploads`、`.upload-locks` 与逐执行 `state.json` 均会照常启动;现于创建会话和媒体状态前只读检查这些遗留标记,发现时明确拒绝启动并保留原文件。测试核实没有写新会话、没有修改标记;仅对配置的恢复根目录生效,不替代现存数据的人工核查或处置。
|
||||
|
||||
## 验收台账
|
||||
|
||||
|
||||
Reference in New Issue
Block a user