Validate Mock Dispatcher startup environment without defaults
This commit is contained in:
@@ -32,6 +32,8 @@
|
||||
- 原 Buf STANDARD 的 `PACKAGE_VERSION_SUFFIX` 与已批准的无代次内部包名冲突;`buf.yaml` 仅对这一条规则作例外,其余 STANDARD 规则保持。隔离安装 Buf v1.50.0 / protoc-gen-go v1.36.12 / protoc-gen-go-grpc v1.5.1 于 `/tmp/sip-go-agent-tools/bin`,未改应用依赖。
|
||||
- `PATH=/tmp/sip-go-agent-tools/bin:$PATH sh scripts/check-proto.sh` 通过(lint/build/generate/新包测试/7 文件清单 hash);`git diff --check`、`go test ./internal/rpc ./internal/agent ./internal/dispatcher ./cmd/sip-go-agent -count=1` 通过。其他旧实现/契约入口、文件名碰撞和迁移数据安全仍待 P02/P07,**不能据此称全仓已无代次或整体完成**。
|
||||
|
||||
- 新增无实现代次字段的 Dispatcher 环境预检:必须显式给出规范 UUID v4 身份、只读 HTTP 地址及密钥、RabbitMQ 地址和 SQLite 路径;当前 Mock 仅接受本机 HTTP/MQ 目标,mixed/real 在任何资源操作前拒绝。预检不打开数据库或网络,缺失值不继承旧默认配置,错误不打印凭据。`go test ./internal/config -run '^TestLoadDispatcherEnvironment' -count=1` 通过;此预检尚未接入主 CLI,不能当作 P02/P03 完成。
|
||||
|
||||
## P03:HTTP 读取分批改造(未整体签收)
|
||||
|
||||
- `contract.ValidateCurrent` 与 `configread` 按当前 Schema 读取 SIP、provider、task、quota 和 cursor 任务发现;严格检查数字 tenant_id、本 D 归属及不可变配置。provider 凭据原值只保留在内存快照,不写日志;Agent 参数中的显式 0/false 保真;无旧 Schema/旧配置回退。
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.ipao.vip/rogee/go-sip/internal/tenant"
|
||||
)
|
||||
|
||||
// DispatcherEnvironment contains only deployment-owned startup values. The
|
||||
// SaaS read secret is never a task/AI setting and must not be logged.
|
||||
type DispatcherEnvironment struct {
|
||||
DispatcherID string
|
||||
SecretKey string
|
||||
SaaSBaseURL string
|
||||
RabbitMQURL string
|
||||
SQLitePath string
|
||||
}
|
||||
|
||||
// LoadDispatcherEnvironment is pure inspection: it opens no database, queue or
|
||||
// network connection. The current executable accepts only isolated Mock mode.
|
||||
func LoadDispatcherEnvironment(mode string) (DispatcherEnvironment, error) {
|
||||
if mode != "mock" {
|
||||
return DispatcherEnvironment{}, errors.New("Dispatcher 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 DispatcherEnvironment
|
||||
var err error
|
||||
if settings.DispatcherID, err = get("DISPATCHER_ID"); err != nil {
|
||||
return DispatcherEnvironment{}, err
|
||||
}
|
||||
if tenant.ValidateDispatcherID(settings.DispatcherID) != nil {
|
||||
return DispatcherEnvironment{}, errors.New("DISPATCHER_ID must be a canonical UUID v4")
|
||||
}
|
||||
if settings.SecretKey, err = get("DISPATCHER_SECRET_KEY"); err != nil {
|
||||
return DispatcherEnvironment{}, err
|
||||
}
|
||||
if settings.SaaSBaseURL, err = get("SAAS_BASE_URL"); err != nil {
|
||||
return DispatcherEnvironment{}, err
|
||||
}
|
||||
if !localEndpoint(settings.SaaSBaseURL, "http", "https") {
|
||||
return DispatcherEnvironment{}, errors.New("SAAS_BASE_URL must name a local Mock HTTP endpoint")
|
||||
}
|
||||
if settings.RabbitMQURL, err = get("RABBITMQ_URL"); err != nil {
|
||||
return DispatcherEnvironment{}, err
|
||||
}
|
||||
if !localEndpoint(settings.RabbitMQURL, "amqp", "amqps") {
|
||||
return DispatcherEnvironment{}, errors.New("RABBITMQ_URL must name a local Mock AMQP endpoint")
|
||||
}
|
||||
if settings.SQLitePath, err = get("DISPATCHER_SQLITE_PATH"); err != nil {
|
||||
return DispatcherEnvironment{}, err
|
||||
}
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
func localEndpoint(raw string, schemes ...string) bool {
|
||||
parsed, err := url.Parse(raw)
|
||||
if err != nil || parsed.Hostname() == "" || parsed.Opaque != "" || parsed.Fragment != "" || parsed.RawQuery != "" {
|
||||
return false
|
||||
}
|
||||
validScheme := false
|
||||
for _, scheme := range schemes {
|
||||
validScheme = validScheme || parsed.Scheme == scheme
|
||||
}
|
||||
if !validScheme {
|
||||
return false
|
||||
}
|
||||
host := parsed.Hostname()
|
||||
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"
|
||||
)
|
||||
|
||||
const dispatcherFixtureID = "c046b893-8628-4589-ae50-619d049248a6"
|
||||
|
||||
func setDispatcherEnvironment(t *testing.T) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "dispatcher.db")
|
||||
t.Setenv("DISPATCHER_ID", dispatcherFixtureID)
|
||||
t.Setenv("DISPATCHER_SECRET_KEY", "synthetic-placeholder-not-a-credential")
|
||||
t.Setenv("SAAS_BASE_URL", "http://127.0.0.1:8080")
|
||||
t.Setenv("RABBITMQ_URL", "amqp://127.0.0.1:5672/%2Fmock")
|
||||
t.Setenv("DISPATCHER_SQLITE_PATH", path)
|
||||
return path
|
||||
}
|
||||
|
||||
func TestLoadDispatcherEnvironmentRefusesNonMockBeforeResources(t *testing.T) {
|
||||
for _, mode := range []string{"real", "mixed", ""} {
|
||||
t.Run(mode, func(t *testing.T) {
|
||||
path := setDispatcherEnvironment(t)
|
||||
if _, err := LoadDispatcherEnvironment(mode); err == nil || !strings.Contains(err.Error(), "Mock") {
|
||||
t.Fatalf("unapproved mode was admitted: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
t.Fatalf("mode rejection opened a database: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadDispatcherEnvironmentRequiresExplicitIdentityAndNoFallback(t *testing.T) {
|
||||
for _, name := range []string{"DISPATCHER_ID", "DISPATCHER_SECRET_KEY", "SAAS_BASE_URL", "RABBITMQ_URL", "DISPATCHER_SQLITE_PATH"} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
setDispatcherEnvironment(t)
|
||||
t.Setenv(name, "")
|
||||
if _, err := LoadDispatcherEnvironment("mock"); err == nil || !strings.Contains(err.Error(), name) {
|
||||
t.Fatalf("missing current config was silently defaulted: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
setDispatcherEnvironment(t)
|
||||
t.Setenv("DISPATCHER_ID", "local-dispatcher")
|
||||
if _, err := LoadDispatcherEnvironment("mock"); err == nil || !strings.Contains(err.Error(), "DISPATCHER_ID") {
|
||||
t.Fatalf("non-UUID dispatcher identity was admitted: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadDispatcherEnvironmentKeepsMockTransportsLocalAndErrorsRedacted(t *testing.T) {
|
||||
for _, tc := range []struct{ name, value string }{
|
||||
{"SAAS_BASE_URL", "https://saas.example.invalid"},
|
||||
{"RABBITMQ_URL", "amqps://mq.example.invalid:5671/%2F"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
setDispatcherEnvironment(t)
|
||||
t.Setenv(tc.name, tc.value)
|
||||
_, err := LoadDispatcherEnvironment("mock")
|
||||
if err == nil || !strings.Contains(err.Error(), tc.name) || strings.Contains(err.Error(), "synthetic-placeholder-not-a-credential") || strings.Contains(err.Error(), tc.value) {
|
||||
t.Fatalf("remote Mock endpoint or credential disclosure was allowed: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadDispatcherEnvironmentPreservesExplicitValuesWithoutOpeningSQLite(t *testing.T) {
|
||||
path := setDispatcherEnvironment(t)
|
||||
settings, err := LoadDispatcherEnvironment("mock")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if settings.DispatcherID != dispatcherFixtureID || settings.SecretKey != "synthetic-placeholder-not-a-credential" || settings.SaaSBaseURL != "http://127.0.0.1:8080" || settings.RabbitMQURL != "amqp://127.0.0.1:5672/%2Fmock" || settings.SQLitePath != path {
|
||||
t.Fatal("explicit current environment values were changed")
|
||||
}
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
t.Fatalf("config inspection opened the durable business database: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user