Validate explicit isolated Agent runtime environment
This commit is contained in:
@@ -34,6 +34,8 @@
|
||||
|
||||
- 新增无实现代次字段的 Dispatcher 环境预检:必须显式给出规范 UUID v4 身份、只读 HTTP 地址及密钥、RabbitMQ 地址和 SQLite 路径;当前 Mock 仅接受本机 HTTP/MQ 目标,mixed/real 在任何资源操作前拒绝。预检不打开数据库或网络,缺失值不继承旧默认配置,错误不打印凭据。`go test ./internal/config -run '^TestLoadDispatcherEnvironment' -count=1` 通过;此预检尚未接入主 CLI,不能当作 P02/P03 完成。
|
||||
|
||||
- Agent 增加独立的无代次环境预检:单 Agent/Cell 身份、会话与私有恢复路径、隔离 Mock 场景和已加载 SIP 测试事实、D gRPC 目标及 mTLS 文件/指纹均须显式提供;仅允许本机监听和本机 D 端点,mixed/real 在访问文件或网络前拒绝。缺失项不继承旧 `FromEnv` 默认值,凭据/地址不回显;`go test ./internal/config -run '^TestLoadAgentEnvironment' -count=1` 通过。此检查尚未接入主 `agent` 命令,也不能证明实际 TLS 或 Asterisk 加载。
|
||||
|
||||
## P03:HTTP 读取分批改造(未整体签收)
|
||||
|
||||
- `contract.ValidateCurrent` 与 `configread` 按当前 Schema 读取 SIP、provider、task、quota 和 cursor 任务发现;严格检查数字 tenant_id、本 D 归属及不可变配置。provider 凭据原值只保留在内存快照,不写日志;Agent 参数中的显式 0/false 保真;无旧 Schema/旧配置回退。
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// AgentEnvironment is deployment-owned and is inspected without opening any
|
||||
// durable file, media adapter, credential or network connection.
|
||||
type AgentEnvironment struct {
|
||||
AgentID string
|
||||
CellID string
|
||||
Listen string
|
||||
SessionPath string
|
||||
RecoveryRoot string
|
||||
DispatcherEndpoint string
|
||||
DispatcherServerName string
|
||||
CAFile string
|
||||
CertFile string
|
||||
KeyFile string
|
||||
PeerFingerprints map[string]struct{}
|
||||
MockScenarioFile string
|
||||
MockAppliedSIPFile string
|
||||
}
|
||||
|
||||
func LoadAgentEnvironment(mode string) (AgentEnvironment, error) {
|
||||
if mode != "mock" {
|
||||
return AgentEnvironment{}, errors.New("Agent accepts only isolated Mock mode")
|
||||
}
|
||||
get := func(name string) (string, error) {
|
||||
value := os.Getenv(name)
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return "", fmt.Errorf("%s is required", name)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
var settings AgentEnvironment
|
||||
for _, field := range []struct {
|
||||
name string
|
||||
value *string
|
||||
}{
|
||||
{"AGENT_ID", &settings.AgentID}, {"CELL_ID", &settings.CellID},
|
||||
{"AGENT_GRPC_LISTEN", &settings.Listen}, {"AGENT_SESSION_PATH", &settings.SessionPath},
|
||||
{"AGENT_RECOVERY_ROOT", &settings.RecoveryRoot},
|
||||
{"DISPATCHER_GRPC_ENDPOINT", &settings.DispatcherEndpoint},
|
||||
{"DISPATCHER_GRPC_SERVER_NAME", &settings.DispatcherServerName},
|
||||
{"MTLS_CA_FILE", &settings.CAFile}, {"MTLS_CERT_FILE", &settings.CertFile},
|
||||
{"MTLS_KEY_FILE", &settings.KeyFile},
|
||||
{"AGENT_MOCK_SCENARIO_FILE", &settings.MockScenarioFile},
|
||||
{"AGENT_MOCK_APPLIED_SIP_FILE", &settings.MockAppliedSIPFile},
|
||||
} {
|
||||
value, err := get(field.name)
|
||||
if err != nil {
|
||||
return AgentEnvironment{}, err
|
||||
}
|
||||
*field.value = value
|
||||
}
|
||||
if !localGRPCAddress(settings.Listen, true) {
|
||||
return AgentEnvironment{}, errors.New("AGENT_GRPC_LISTEN must be an isolated local Mock address")
|
||||
}
|
||||
if !localGRPCAddress(settings.DispatcherEndpoint, false) {
|
||||
return AgentEnvironment{}, errors.New("DISPATCHER_GRPC_ENDPOINT must be an isolated local Mock address")
|
||||
}
|
||||
rawFingerprints, err := get("MTLS_PEER_CERT_FINGERPRINTS")
|
||||
if err != nil {
|
||||
return AgentEnvironment{}, err
|
||||
}
|
||||
settings.PeerFingerprints, err = ParseCertificateFingerprints(rawFingerprints)
|
||||
if err != nil || len(settings.PeerFingerprints) == 0 {
|
||||
return AgentEnvironment{}, errors.New("MTLS_PEER_CERT_FINGERPRINTS is invalid")
|
||||
}
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
func localGRPCAddress(raw string, allowZeroPort bool) bool {
|
||||
host, port, err := net.SplitHostPort(raw)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
portNumber, err := strconv.Atoi(port)
|
||||
if err != nil || portNumber < 0 || portNumber > 65535 || (!allowZeroPort && portNumber == 0) {
|
||||
return false
|
||||
}
|
||||
if strings.EqualFold(host, "localhost") {
|
||||
return true
|
||||
}
|
||||
ip := net.ParseIP(host)
|
||||
return ip != nil && ip.IsLoopback()
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func setAgentEnvironment(t *testing.T) string {
|
||||
t.Helper()
|
||||
root := t.TempDir()
|
||||
state := filepath.Join(root, "session.json")
|
||||
for name, value := range map[string]string{
|
||||
"AGENT_ID": "agent-mock", "CELL_ID": "cell-mock", "AGENT_GRPC_LISTEN": "127.0.0.1:0",
|
||||
"AGENT_SESSION_PATH": state, "AGENT_RECOVERY_ROOT": filepath.Join(root, "recovery"),
|
||||
"DISPATCHER_GRPC_ENDPOINT": "127.0.0.1:39443", "DISPATCHER_GRPC_SERVER_NAME": "dispatcher.local",
|
||||
"MTLS_CA_FILE": filepath.Join(root, "ca.pem"), "MTLS_CERT_FILE": filepath.Join(root, "agent.pem"),
|
||||
"MTLS_KEY_FILE": filepath.Join(root, "agent.key"), "MTLS_PEER_CERT_FINGERPRINTS": strings.Repeat("a", 64),
|
||||
"AGENT_MOCK_SCENARIO_FILE": filepath.Join(root, "scenario.json"),
|
||||
"AGENT_MOCK_APPLIED_SIP_FILE": filepath.Join(root, "applied-sip.json"),
|
||||
} {
|
||||
t.Setenv(name, value)
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
func TestLoadAgentEnvironmentRefusesNonMockBeforeResources(t *testing.T) {
|
||||
for _, mode := range []string{"real", "mixed", ""} {
|
||||
t.Run(mode, func(t *testing.T) {
|
||||
state := setAgentEnvironment(t)
|
||||
if _, err := LoadAgentEnvironment(mode); err == nil || !strings.Contains(err.Error(), "Mock") {
|
||||
t.Fatalf("unapproved Agent mode was admitted: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(state); !os.IsNotExist(err) {
|
||||
t.Fatalf("mode refusal opened Agent state: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadAgentEnvironmentRequiresExplicitDeploymentValues(t *testing.T) {
|
||||
for _, name := range []string{
|
||||
"AGENT_ID", "CELL_ID", "AGENT_GRPC_LISTEN", "AGENT_SESSION_PATH", "AGENT_RECOVERY_ROOT",
|
||||
"DISPATCHER_GRPC_ENDPOINT", "DISPATCHER_GRPC_SERVER_NAME", "MTLS_CA_FILE", "MTLS_CERT_FILE",
|
||||
"MTLS_KEY_FILE", "MTLS_PEER_CERT_FINGERPRINTS", "AGENT_MOCK_SCENARIO_FILE", "AGENT_MOCK_APPLIED_SIP_FILE",
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
setAgentEnvironment(t)
|
||||
t.Setenv(name, "")
|
||||
if _, err := LoadAgentEnvironment("mock"); err == nil || !strings.Contains(err.Error(), name) {
|
||||
t.Fatalf("missing current Agent setting silently used a legacy default: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadAgentEnvironmentRequiresLocalMockEndpointsAndDoesNotOpenFiles(t *testing.T) {
|
||||
state := setAgentEnvironment(t)
|
||||
settings, err := LoadAgentEnvironment("mock")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if settings.AgentID != "agent-mock" || settings.CellID != "cell-mock" || settings.Listen != "127.0.0.1:0" || settings.SessionPath != state || settings.DispatcherEndpoint != "127.0.0.1:39443" {
|
||||
t.Fatal("explicit deployment identity or address was rewritten")
|
||||
}
|
||||
if _, err := os.Stat(state); !os.IsNotExist(err) {
|
||||
t.Fatalf("Agent preflight opened durable state: %v", err)
|
||||
}
|
||||
for _, tc := range []struct{ name, value string }{
|
||||
{"AGENT_GRPC_LISTEN", "0.0.0.0:39444"},
|
||||
{"DISPATCHER_GRPC_ENDPOINT", "saas.example.invalid:443"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
setAgentEnvironment(t)
|
||||
t.Setenv(tc.name, tc.value)
|
||||
if _, err := LoadAgentEnvironment("mock"); err == nil || !strings.Contains(err.Error(), tc.name) || strings.Contains(err.Error(), tc.value) {
|
||||
t.Fatalf("external Mock address was accepted or leaked: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user