diff --git a/docs/evidence/saas-dispatcher-implementation.md b/docs/evidence/saas-dispatcher-implementation.md index 9c725fe..bf664f9 100644 --- a/docs/evidence/saas-dispatcher-implementation.md +++ b/docs/evidence/saas-dispatcher-implementation.md @@ -118,6 +118,7 @@ - Agent 旧失败事实分支:旧 Mock 上传失败事实通过已退役的 `ReportExecutionEvent` RPC 回报,现删除该代码及专属测试。现行录音失败、未知 PUT、重启恢复和最终结果仍由 `recording_delivery*` 隔离测试覆盖;不复活额外通话事件或把未知上传当作成功。 - 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/来源事实另行辨析。 ## 验收台账 diff --git a/internal/contract/static_artifact.go b/internal/contract/static_artifact.go deleted file mode 100644 index 864bc0b..0000000 --- a/internal/contract/static_artifact.go +++ /dev/null @@ -1,147 +0,0 @@ -package contract - -import ( - "encoding/json" - "fmt" -) - -// StaticCellArtifact is the management-approved, immutable Cell/SIP hand-off -// artifact. Its project-local JSON shape is static-cell-artifact-v0.2.schema.json; this -// type only provides a typed boundary after schema validation. -type StaticCellArtifact struct { - ArtifactID string `json:"artifact_id"` - SourceRelease string `json:"source_release"` - SourceDigest string `json:"source_digest"` - ApprovalReference string `json:"approval_reference"` - CellID string `json:"cell_id"` - Revision uint64 `json:"revision"` - ConfigSHA256 string `json:"config_sha256"` - Mode string `json:"mode"` - AllowedTargets []string `json:"allowed_targets"` - Trunks []StaticTrunk `json:"trunks"` - ARI *StaticARI `json:"ari,omitempty"` - MediaProfiles map[string]StaticMediaProfile `json:"media_profiles,omitempty"` - Media *StaticMedia `json:"media,omitempty"` - Recording *StaticRecording `json:"recording,omitempty"` - LoadEvidence *StaticLoadEvidence `json:"load_evidence"` -} - -type StaticTrunk struct { - TrunkID string `json:"trunk_id"` - ProviderID string `json:"provider_id"` - Codec string `json:"codec"` - CallerProfileIDs []string `json:"caller_profile_ids"` - DialPrefix string `json:"dial_prefix"` - Enabled bool `json:"enabled"` - SIPEndpointRef string `json:"sip_endpoint_ref"` - CredentialRef *string `json:"credential_ref"` - MediaProfileID string `json:"media_profile_id,omitempty"` -} - -type StaticARI struct { - BaseURL string `json:"base_url"` - WebsocketURL string `json:"websocket_url"` - Application string `json:"application"` - CredentialRef string `json:"credential_ref"` -} - -type StaticMedia struct { - BindAddress string `json:"bind_address"` - Port int `json:"port"` - Format string `json:"format"` - SampleRateHz int `json:"sample_rate_hz"` - Channels int `json:"channels"` - PayloadType int `json:"payload_type"` -} - -type StaticMediaProfile struct { - Format string `json:"format"` - SampleRateHz int `json:"sample_rate_hz"` - Channels int `json:"channels"` - PayloadType int `json:"payload_type"` -} - -type StaticRecording struct { - Enabled bool `json:"enabled"` - Format string `json:"format"` - Directory string `json:"directory"` - MaxBytes int64 `json:"max_bytes"` -} - -type StaticLoadEvidence struct { - AsteriskConfigSHA256 string `json:"asterisk_config_sha256"` - LoadedAt string `json:"loaded_at"` - Status string `json:"status"` -} - -// StaticArtifactExpectation contains deployment-local binding constraints. -// Empty string/slice values leave the corresponding optional check disabled; -// the source contract remains mandatory and is always validated first. -type StaticArtifactExpectation struct { - CellID string - Mode string - SourceRelease string - SourceDigest string - ConfigSHA256 string - MinimumRevision uint64 - RequiredTrunkIDs []string -} - -// ValidateStaticArtifact validates the versioned artifact schema and then -// applies the local Cell hand-off bindings. It deliberately does not claim -// that Asterisk has loaded the artifact: load_evidence.status is explicitly -// "not-yet-loaded" in the contract until an independent load check exists. -func ValidateStaticArtifact(raw []byte, expected StaticArtifactExpectation) (StaticCellArtifact, error) { - if err := validateLocalSchema("static-cell-artifact-v0.2.schema.json", raw); err != nil { - return StaticCellArtifact{}, err - } - - var artifact StaticCellArtifact - if err := json.Unmarshal(raw, &artifact); err != nil { - return StaticCellArtifact{}, fmt.Errorf("decode static Cell artifact: %w", err) - } - if expected.CellID != "" && artifact.CellID != expected.CellID { - return StaticCellArtifact{}, fmt.Errorf("static artifact cell binding mismatch: got %q, want %q", artifact.CellID, expected.CellID) - } - if expected.Mode != "" && artifact.Mode != expected.Mode { - return StaticCellArtifact{}, fmt.Errorf("static artifact mode mismatch: got %q, want %q", artifact.Mode, expected.Mode) - } - if expected.SourceRelease != "" && artifact.SourceRelease != expected.SourceRelease { - return StaticCellArtifact{}, fmt.Errorf("static artifact source release mismatch: got %q, want %q", artifact.SourceRelease, expected.SourceRelease) - } - if expected.SourceDigest != "" && artifact.SourceDigest != expected.SourceDigest { - return StaticCellArtifact{}, fmt.Errorf("static artifact source digest mismatch: got %q, want %q", artifact.SourceDigest, expected.SourceDigest) - } - if expected.ConfigSHA256 != "" && artifact.ConfigSHA256 != expected.ConfigSHA256 { - return StaticCellArtifact{}, fmt.Errorf("static artifact config digest mismatch: got %q, want %q", artifact.ConfigSHA256, expected.ConfigSHA256) - } - if expected.MinimumRevision != 0 && artifact.Revision < expected.MinimumRevision { - return StaticCellArtifact{}, fmt.Errorf("static artifact revision %d is older than required %d", artifact.Revision, expected.MinimumRevision) - } - - requiredTrunks := make(map[string]struct{}, len(expected.RequiredTrunkIDs)) - for _, trunkID := range expected.RequiredTrunkIDs { - requiredTrunks[trunkID] = struct{}{} - } - seenTrunks := make(map[string]struct{}, len(artifact.Trunks)) - for _, trunk := range artifact.Trunks { - if _, duplicate := seenTrunks[trunk.TrunkID]; duplicate { - return StaticCellArtifact{}, fmt.Errorf("static artifact contains duplicate trunk_id %q", trunk.TrunkID) - } - seenTrunks[trunk.TrunkID] = struct{}{} - if _, required := requiredTrunks[trunk.TrunkID]; required && !trunk.Enabled { - return StaticCellArtifact{}, fmt.Errorf("required static artifact trunk %q is disabled", trunk.TrunkID) - } - if trunk.MediaProfileID != "" { - if _, ok := artifact.MediaProfiles[trunk.MediaProfileID]; !ok { - return StaticCellArtifact{}, fmt.Errorf("static artifact trunk %q references unknown media profile %q", trunk.TrunkID, trunk.MediaProfileID) - } - } - } - for trunkID := range requiredTrunks { - if _, present := seenTrunks[trunkID]; !present { - return StaticCellArtifact{}, fmt.Errorf("required static artifact trunk %q is missing", trunkID) - } - } - return artifact, nil -} diff --git a/internal/contract/static_artifact_test.go b/internal/contract/static_artifact_test.go deleted file mode 100644 index c9aaa8b..0000000 --- a/internal/contract/static_artifact_test.go +++ /dev/null @@ -1,141 +0,0 @@ -package contract - -import ( - "encoding/json" - "testing" - - "git.ipao.vip/rogee/go-sip/contracts" -) - -func TestValidateStaticArtifactBindsCellAndTrunks(t *testing.T) { - raw, err := contracts.Files.ReadFile("local/v0.3/examples/static-cell-artifact-v0.2.json") - if err != nil { - t.Fatal(err) - } - - artifact, err := ValidateStaticArtifact(raw, StaticArtifactExpectation{ - CellID: "cell-a", - Mode: "mock", - SourceRelease: "management-snapshot-1", - SourceDigest: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - ConfigSHA256: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - MinimumRevision: 1, - RequiredTrunkIDs: []string{"trunk-mock"}, - }) - if err != nil { - t.Fatalf("valid artifact rejected: %v", err) - } - if artifact.CellID != "cell-a" || artifact.Revision != 1 || len(artifact.Trunks) != 1 { - t.Fatalf("unexpected artifact: %+v", artifact) - } -} - -func TestValidateRealStaticArtifact(t *testing.T) { - raw, err := contracts.Files.ReadFile("local/v0.3/examples/static-cell-artifact-real-v2.json") - if err != nil { - t.Fatal(err) - } - artifact, err := ValidateStaticArtifact(raw, StaticArtifactExpectation{ - CellID: "cell-single", - Mode: "real", - SourceRelease: "asterisk-22.10.1-native-v1", - SourceDigest: "68006a1a8efed288be4ca4a2ae3cb9554a31d733eac08eaacf4c646c95faf74d", - ConfigSHA256: "89d2686d0d1ca60159c3c6bd725dc9e6f511cbdb56bf6ce7b65ca7d4dc3f2d60", - RequiredTrunkIDs: []string{"provider-second"}, - }) - if err != nil { - t.Fatalf("valid real artifact rejected: %v", err) - } - if artifact.ARI == nil || artifact.Media == nil || artifact.Recording == nil { - t.Fatalf("real artifact lost runtime sections: %+v", artifact) - } - if artifact.Media.Format != "alaw" || artifact.Media.SampleRateHz != 8000 || artifact.Media.PayloadType != 8 || artifact.Recording.Format != "wav" { - t.Fatalf("unexpected real media/recording contract: %+v %+v", artifact.Media, artifact.Recording) - } -} - -func TestValidateStaticArtifactRejectsBindingViolations(t *testing.T) { - raw, err := contracts.Files.ReadFile("local/v0.3/examples/static-cell-artifact-v0.2.json") - if err != nil { - t.Fatal(err) - } - base := func() StaticArtifactExpectation { - return StaticArtifactExpectation{ - CellID: "cell-a", - Mode: "mock", - SourceRelease: "management-snapshot-1", - SourceDigest: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - ConfigSHA256: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - } - } - - tests := []struct { - name string - expected StaticArtifactExpectation - mutate func(*StaticCellArtifact) - }{ - {name: "wrong cell", expected: func() StaticArtifactExpectation { e := base(); e.CellID = "cell-b"; return e }()}, - {name: "wrong source digest", expected: func() StaticArtifactExpectation { - e := base() - e.SourceDigest = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - return e - }()}, - {name: "old revision", expected: func() StaticArtifactExpectation { e := base(); e.MinimumRevision = 2; return e }()}, - {name: "missing required trunk", expected: func() StaticArtifactExpectation { - e := base() - e.RequiredTrunkIDs = []string{"trunk-required"} - return e - }()}, - {name: "disabled required trunk", expected: func() StaticArtifactExpectation { e := base(); e.RequiredTrunkIDs = []string{"trunk-mock"}; return e }(), mutate: func(a *StaticCellArtifact) { a.Trunks[0].Enabled = false }}, - {name: "duplicate trunk", expected: base(), mutate: func(a *StaticCellArtifact) { a.Trunks = append(a.Trunks, a.Trunks[0]) }}, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - candidate := raw - if test.mutate != nil { - var artifact StaticCellArtifact - if err := json.Unmarshal(raw, &artifact); err != nil { - t.Fatal(err) - } - test.mutate(&artifact) - candidate, err = json.Marshal(artifact) - if err != nil { - t.Fatal(err) - } - } - if _, err := ValidateStaticArtifact(candidate, test.expected); err == nil { - t.Fatal("expected static artifact validation to fail") - } - }) - } -} - -func TestValidateStaticArtifactRejectsRemovedEgressPoolField(t *testing.T) { - old, err := contracts.Files.ReadFile("upstream/v1/examples/static-cell-artifact.json") - if err != nil { - t.Fatal(err) - } - if _, err := ValidateStaticArtifact(old, StaticArtifactExpectation{}); err == nil { - t.Fatal("legacy egress-pool field unexpectedly accepted") - } -} - -func TestValidateStaticArtifactAlwaysChecksLocalSchema(t *testing.T) { - raw, err := contracts.Files.ReadFile("local/v0.3/examples/static-cell-artifact-v0.2.json") - if err != nil { - t.Fatal(err) - } - var value map[string]any - if err := json.Unmarshal(raw, &value); err != nil { - t.Fatal(err) - } - value["unexpected"] = true - candidate, err := json.Marshal(value) - if err != nil { - t.Fatal(err) - } - if _, err := ValidateStaticArtifact(candidate, StaticArtifactExpectation{}); err == nil { - t.Fatal("expected schema validation failure") - } -} diff --git a/internal/rpc/server.go b/internal/rpc/server.go index 60995f3..1febc85 100644 --- a/internal/rpc/server.go +++ b/internal/rpc/server.go @@ -13,7 +13,6 @@ import ( agentpb "git.ipao.vip/rogee/go-sip/gen/agent" "git.ipao.vip/rogee/go-sip/internal/agent" - "git.ipao.vip/rogee/go-sip/internal/contract" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" @@ -27,8 +26,6 @@ import ( type ServerOptions struct { Mode string Status *agentpb.AgentStatus - StaticArtifactRaw []byte - StaticArtifactExpected contract.StaticArtifactExpectation Now func() time.Time RequirePeerCertificate bool ApprovedDispatcherID string @@ -51,9 +48,6 @@ type Server struct { mode string now func() time.Time status *agentpb.AgentStatus - staticArtifact contract.StaticCellArtifact - staticArtifactEnabled bool - staticArtifactError error requirePeerCertificate bool approvedDispatcherID string peerAgentIDs map[string]string @@ -84,19 +78,10 @@ func NewServer(options ServerOptions) *Server { if statusValue.AdmissionState == agentpb.AdmissionState_ADMISSION_STATE_UNSPECIFIED { statusValue.AdmissionState = agentpb.AdmissionState_ADMISSION_STATE_CLOSED } - var staticArtifact contract.StaticCellArtifact - var staticArtifactError error - staticArtifactEnabled := len(options.StaticArtifactRaw) != 0 - if staticArtifactEnabled { - staticArtifact, staticArtifactError = contract.ValidateStaticArtifact(options.StaticArtifactRaw, options.StaticArtifactExpected) - } server := &Server{ mode: mode, now: now, status: statusValue, - staticArtifact: staticArtifact, - staticArtifactEnabled: staticArtifactEnabled, - staticArtifactError: staticArtifactError, requirePeerCertificate: options.RequirePeerCertificate, approvedDispatcherID: options.ApprovedDispatcherID, peerAgentIDs: cloneStringMap(options.PeerAgentIDs), @@ -350,14 +335,6 @@ func (s *Server) ActivateAgent(ctx context.Context, req *agentpb.ActivateAgentRe if s.approvedDispatcherID != "" && req.Binding.DispatcherId != s.approvedDispatcherID { return nil, status.Error(codes.PermissionDenied, "activated Dispatcher identity is not authorized") } - if s.staticArtifactEnabled { - if s.staticArtifactError != nil { - return nil, status.Errorf(codes.FailedPrecondition, "static Cell artifact is invalid: %v", s.staticArtifactError) - } - if req.Binding.CellId != s.staticArtifact.CellID { - return nil, status.Error(codes.FailedPrecondition, "activation Cell does not match static artifact") - } - } binding := proto.Clone(req.Binding).(*agentpb.AgentBinding) if binding.ExpectedBootId == "" { binding.ExpectedBootId = req.Meta.BootId diff --git a/internal/rpc/service_test.go b/internal/rpc/service_test.go index 085f35b..0cbda69 100644 --- a/internal/rpc/service_test.go +++ b/internal/rpc/service_test.go @@ -22,6 +22,15 @@ func TestAgentControlServiceOnlyExposesApprovedMethods(t *testing.T) { } } +func TestAgentServerUsesLoadedSIPInsteadOfRetiredStaticArtifact(t *testing.T) { + options := reflect.TypeOf(ServerOptions{}) + for _, name := range []string{"StaticArtifactRaw", "StaticArtifactExpected"} { + if _, exists := options.FieldByName(name); exists { + t.Fatalf("retired static artifact input %s remains on the Agent server", name) + } + } +} + func TestAgentServerHasNoRetiredBusinessMethods(t *testing.T) { server := reflect.TypeOf(&Server{}) for _, name := range []string{ diff --git a/internal/rpc/static_artifact_test.go b/internal/rpc/static_artifact_test.go deleted file mode 100644 index c906d31..0000000 --- a/internal/rpc/static_artifact_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package rpc - -import ( - "context" - "testing" - "time" - - "git.ipao.vip/rogee/go-sip/contracts" - agentpb "git.ipao.vip/rogee/go-sip/gen/agent" - "git.ipao.vip/rogee/go-sip/internal/contract" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" -) - -func TestActivationValidatesStaticCellArtifact(t *testing.T) { - raw, err := contracts.Files.ReadFile("local/v0.3/examples/static-cell-artifact-v0.2.json") - if err != nil { - t.Fatal(err) - } - server := NewServer(ServerOptions{ - Now: func() time.Time { return time.Unix(100, 0) }, - StaticArtifactRaw: raw, - StaticArtifactExpected: contract.StaticArtifactExpectation{ - CellID: "cell-a", - Mode: "mock", - SourceRelease: "management-snapshot-1", - SourceDigest: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - ConfigSHA256: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - RequiredTrunkIDs: []string{"trunk-mock"}, - }, - }) - meta := testMeta("activate-static", "", 0) - meta.CellId = "cell-a" - response, err := server.ActivateAgent(context.Background(), &agentpb.ActivateAgentRequest{ - Meta: meta, - Binding: &agentpb.AgentBinding{AgentId: "agent-1", CellId: "cell-a", ExpectedBootId: "boot-1", DispatcherEpoch: "epoch-1", SessionGeneration: 1}, - ActivationOperationId: "activate-static", - }) - if err != nil { - t.Fatal(err) - } - if response.State != agentpb.ActivationState_ACTIVATION_STATE_ACTIVE { - t.Fatalf("activation state=%s", response.State) - } -} - -func TestActivationRejectsInvalidStaticCellArtifact(t *testing.T) { - raw, err := contracts.Files.ReadFile("local/v0.3/examples/static-cell-artifact-v0.2.json") - if err != nil { - t.Fatal(err) - } - server := NewServer(ServerOptions{ - Now: func() time.Time { return time.Unix(100, 0) }, - StaticArtifactRaw: raw, - StaticArtifactExpected: contract.StaticArtifactExpectation{CellID: "cell-b", Mode: "mock"}, - }) - meta := testMeta("activate-invalid-static", "", 0) - meta.CellId = "cell-a" - _, err = server.ActivateAgent(context.Background(), &agentpb.ActivateAgentRequest{ - Meta: meta, - Binding: &agentpb.AgentBinding{AgentId: "agent-1", CellId: "cell-a", ExpectedBootId: "boot-1", DispatcherEpoch: "epoch-1", SessionGeneration: 1}, - ActivationOperationId: "activate-invalid-static", - }) - if err == nil || status.Code(err) != codes.FailedPrecondition { - t.Fatalf("invalid artifact error=%v code=%s", err, status.Code(err)) - } -}