Validate explicit isolated Agent runtime environment

This commit is contained in:
2026-09-30 04:53:22 +08:00
parent 5d5861c872
commit 2b09ba30ca
3 changed files with 177 additions and 0 deletions
@@ -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/旧配置回退。
+93
View File
@@ -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()
}
+82
View File
@@ -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)
}
})
}
}