package config import ( "encoding/json" "errors" "flag" "os" "path/filepath" "strconv" "strings" "time" ) const MaxMeshProductionObservationSinkCapacity = 10000 type Config struct { ClusterID string ClusterAuthorityPublicKey string ClusterAuthorityFingerprint string JoinToken string NodeName string StateDir string WorkloadSupervisionEnabled bool WebIngressRuntimeEnabled bool WebIngressSigningPrivateKey string WebIngressSigningKeyID string WebIngressTrustedKeysJSON string WebIngressRuntimeServiceClasses string HeartbeatInterval time.Duration EnrollmentPollInterval time.Duration EnrollmentPollTimeout time.Duration FabricRuntimeEnabled bool MeshProductionForwardingEnabled bool VPNFabricSessionTransportEnabled bool MeshQUICFabricEnabled bool MeshQUICFabricListenAddr string VPNFabricSessionStreamShards int VPNFabricQUICMaxStreamsPerConn int VPNFabricQUICIdleTTL time.Duration MeshProductionObservationSinkCapacity int FabricListenAddr string FabricListenPortMode string FabricListenAutoPortStart int FabricListenAutoPortEnd int MeshAdvertiseEndpoint string MeshAdvertiseEndpointsJSON string FabricRegistryRecordsJSON string MeshAdvertiseTransport string MeshConnectivityMode string MeshNATType string MeshSiteID string MeshLocalityGroupID string MeshNATGroupID string MeshSTUNReflexiveEndpoint string MeshSTUNServer string MeshRelayNodeID string MeshRelayEndpoint string MeshRegion string MeshSyntheticConfigPath string MeshPeerEndpointsJSON string MeshSyntheticRoutesJSON string RemoteWorkspaceRealAdapterEnabled bool RemoteWorkspaceRealAdapterCommand string RemoteWorkspaceRealAdapterArgsJSON string RemoteWorkspaceRealAdapterWorkDir string } func Load(args []string, env map[string]string) (Config, error) { if env == nil { env = readEnv() } defaultStateDir := filepath.Join(".", ".rap-node-agent") fs := flag.NewFlagSet("rap-node-agent", flag.ContinueOnError) cfg := Config{} fs.StringVar(&cfg.ClusterID, "cluster-id", getEnv(env, "RAP_CLUSTER_ID", ""), "Cluster ID.") fs.StringVar(&cfg.ClusterAuthorityPublicKey, "cluster-authority-public-key", getEnv(env, "RAP_CLUSTER_AUTHORITY_PUBLIC_KEY", ""), "Pinned cluster authority Ed25519 public key.") fs.StringVar(&cfg.ClusterAuthorityFingerprint, "cluster-authority-fingerprint", getEnv(env, "RAP_CLUSTER_AUTHORITY_FINGERPRINT", ""), "Pinned cluster authority key fingerprint.") fs.StringVar(&cfg.JoinToken, "join-token", getEnv(env, "RAP_JOIN_TOKEN", ""), "Short-lived node join token.") fs.StringVar(&cfg.NodeName, "node-name", getEnv(env, "RAP_NODE_NAME", hostnameOrDefault()), "Node display name.") fs.StringVar(&cfg.StateDir, "state-dir", getEnv(env, "RAP_NODE_STATE_DIR", defaultStateDir), "Local node-agent state directory.") fs.BoolVar(&cfg.WorkloadSupervisionEnabled, "workload-supervision-enabled", getEnvBool(env, "RAP_WORKLOAD_SUPERVISION_ENABLED", false), "Enable desired workload polling and status reporting. Disabled by default while service runtime is not implemented.") fs.BoolVar(&cfg.WebIngressRuntimeEnabled, "web-ingress-runtime-enabled", getEnvBool(env, "RAP_WEB_INGRESS_RUNTIME_ENABLED", false), "Enable the future real 80/443 web ingress listener runtime. Disabled by default; contract probe remains safe without it.") fs.StringVar(&cfg.WebIngressSigningPrivateKey, "web-ingress-signing-private-key", getEnv(env, "RAP_WEB_INGRESS_SIGNING_PRIVATE_KEY", ""), "Base64 Ed25519 private key used to sign web ingress fabric envelopes. Empty keeps signing disabled.") fs.StringVar(&cfg.WebIngressSigningKeyID, "web-ingress-signing-key-id", getEnv(env, "RAP_WEB_INGRESS_SIGNING_KEY_ID", ""), "Optional key id for web ingress envelope signatures.") fs.StringVar(&cfg.WebIngressTrustedKeysJSON, "web-ingress-trusted-keys-json", getEnv(env, "RAP_WEB_INGRESS_TRUSTED_KEYS_JSON", ""), "JSON map or array of trusted Ed25519 public keys for web ingress runtime receiver.") fs.StringVar(&cfg.WebIngressRuntimeServiceClasses, "web-ingress-runtime-service-classes", getEnv(env, "RAP_WEB_INGRESS_RUNTIME_SERVICE_CLASSES", ""), "Optional comma-separated allow-list of web ingress runtime service classes accepted by this node.") fs.BoolVar(&cfg.FabricRuntimeEnabled, "fabric-runtime-enabled", getEnvBool(env, "RAP_FABRIC_RUNTIME_ENABLED", false), "Enable C17A synthetic fabric probe runtime. Disabled by default.") fs.BoolVar(&cfg.MeshProductionForwardingEnabled, "mesh-production-forwarding-enabled", getEnvBool(env, "RAP_MESH_PRODUCTION_FORWARDING_ENABLED", false), "Enable production fabric-control direct next-hop forwarding gate. Disabled by default.") fs.BoolVar(&cfg.VPNFabricSessionTransportEnabled, "vpn-fabric-session-transport-enabled", getEnvBool(env, "RAP_VPN_FABRIC_SESSION_TRANSPORT_ENABLED", false), "Route VPN packet transport over persistent fabric session when explicitly enabled. Disabled by default.") fs.BoolVar(&cfg.MeshQUICFabricEnabled, "mesh-quic-fabric-enabled", getEnvBool(env, "RAP_MESH_QUIC_FABRIC_ENABLED", false), "Enable QUIC/UDP fabric listener. Disabled by default.") fs.StringVar(&cfg.MeshQUICFabricListenAddr, "mesh-quic-fabric-listen-addr", getEnv(env, "RAP_MESH_QUIC_FABRIC_LISTEN_ADDR", ""), "Listen address for QUIC/UDP fabric endpoint, for example :19443.") fs.IntVar(&cfg.VPNFabricSessionStreamShards, "vpn-fabric-session-stream-shards", getEnvInt(env, "RAP_VPN_FABRIC_SESSION_STREAM_SHARDS", 8), "VPN fabric-session stream shards per traffic class.") fs.IntVar(&cfg.VPNFabricQUICMaxStreamsPerConn, "vpn-fabric-quic-max-streams-per-conn", getEnvInt(env, "RAP_VPN_FABRIC_QUIC_MAX_STREAMS_PER_CONN", 64), "Maximum logical fabric-session streams per cached VPN QUIC carrier connection.") fs.DurationVar(&cfg.VPNFabricQUICIdleTTL, "vpn-fabric-quic-idle-ttl", time.Duration(getEnvInt(env, "RAP_VPN_FABRIC_QUIC_IDLE_TTL_SECONDS", 300))*time.Second, "Idle TTL for cached VPN QUIC carrier connections.") fs.IntVar(&cfg.MeshProductionObservationSinkCapacity, "mesh-production-observation-sink-capacity", getEnvSignedInt(env, "RAP_MESH_PRODUCTION_OBSERVATION_SINK_CAPACITY", 0), "Bounded local metadata-only production envelope observation sink capacity. Disabled when 0.") fs.StringVar(&cfg.FabricListenAddr, "fabric-listen-addr", getEnv(env, "RAP_FABRIC_LISTEN_ADDR", ""), "Optional node listener address used by the QUIC fabric runtime contract.") fs.StringVar(&cfg.FabricListenPortMode, "fabric-listen-port-mode", getEnv(env, "RAP_FABRIC_LISTEN_PORT_MODE", "manual"), "Fabric listen port behavior: manual, auto, or disabled.") fs.IntVar(&cfg.FabricListenAutoPortStart, "fabric-listen-auto-port-start", getEnvInt(env, "RAP_FABRIC_LISTEN_AUTO_PORT_START", 19131), "First port used when fabric listen port mode is auto.") fs.IntVar(&cfg.FabricListenAutoPortEnd, "fabric-listen-auto-port-end", getEnvInt(env, "RAP_FABRIC_LISTEN_AUTO_PORT_END", 19231), "Last port used when fabric listen port mode is auto.") fs.StringVar(&cfg.MeshAdvertiseEndpoint, "mesh-advertise-endpoint", getEnv(env, "RAP_MESH_ADVERTISE_ENDPOINT", ""), "Advertised mesh endpoint reported to the Control Plane. Empty disables endpoint reporting.") fs.StringVar(&cfg.MeshAdvertiseEndpointsJSON, "mesh-advertise-endpoints-json", getEnv(env, "RAP_MESH_ADVERTISE_ENDPOINTS_JSON", ""), "JSON array of advertised mesh endpoint candidates, including private/corporate endpoints.") fs.StringVar(&cfg.FabricRegistryRecordsJSON, "fabric-registry-records-json", getEnv(env, "RAP_FABRIC_REGISTRY_RECORDS_JSON", ""), "JSON array of signed QUIC-only fabric registry gossip records used as bootstrap discovery seeds.") fs.StringVar(&cfg.MeshAdvertiseTransport, "mesh-advertise-transport", getEnv(env, "RAP_MESH_ADVERTISE_TRANSPORT", "quic"), "Transport label for the advertised mesh endpoint.") fs.StringVar(&cfg.MeshConnectivityMode, "mesh-connectivity-mode", getEnv(env, "RAP_MESH_CONNECTIVITY_MODE", "direct"), "Connectivity mode reported with the advertised mesh endpoint.") fs.StringVar(&cfg.MeshNATType, "mesh-nat-type", getEnv(env, "RAP_MESH_NAT_TYPE", "unknown"), "NAT type hint reported with the advertised mesh endpoint.") fs.StringVar(&cfg.MeshSiteID, "mesh-site-id", getEnv(env, "RAP_MESH_SITE_ID", ""), "Optional physical or logical site identifier advertised with QUIC endpoint candidates.") fs.StringVar(&cfg.MeshLocalityGroupID, "mesh-locality-group-id", getEnv(env, "RAP_MESH_LOCALITY_GROUP_ID", ""), "Optional locality group identifier used to decide whether private QUIC endpoints are actually local.") fs.StringVar(&cfg.MeshNATGroupID, "mesh-nat-group-id", getEnv(env, "RAP_MESH_NAT_GROUP_ID", ""), "Optional NAT group ID advertised with QUIC endpoint candidates.") fs.StringVar(&cfg.MeshSTUNReflexiveEndpoint, "mesh-stun-reflexive-endpoint", getEnv(env, "RAP_MESH_STUN_REFLEXIVE_ENDPOINT", ""), "Optional STUN-discovered reflexive QUIC endpoint, for example quic://203.0.113.10:19443.") fs.StringVar(&cfg.MeshSTUNServer, "mesh-stun-server", getEnv(env, "RAP_MESH_STUN_SERVER", ""), "Optional STUN server name used to discover the reflexive endpoint.") fs.StringVar(&cfg.MeshRelayNodeID, "mesh-relay-node-id", getEnv(env, "RAP_MESH_RELAY_NODE_ID", ""), "Optional relay node ID for relay-required QUIC fallback candidates.") fs.StringVar(&cfg.MeshRelayEndpoint, "mesh-relay-endpoint", getEnv(env, "RAP_MESH_RELAY_ENDPOINT", ""), "Optional relay QUIC endpoint for relay-required fallback candidates.") fs.StringVar(&cfg.MeshRegion, "mesh-region", getEnv(env, "RAP_MESH_REGION", ""), "Optional region/site hint for the advertised mesh endpoint.") fs.StringVar(&cfg.MeshSyntheticConfigPath, "mesh-synthetic-config", getEnv(env, "RAP_MESH_SYNTHETIC_CONFIG", ""), "Path to scoped synthetic mesh config snapshot. Preferred over debug JSON env.") fs.StringVar(&cfg.MeshPeerEndpointsJSON, "mesh-peer-endpoints-json", getEnv(env, "RAP_MESH_PEER_ENDPOINTS_JSON", ""), "JSON object mapping peer node_id to synthetic mesh endpoint URL.") fs.StringVar(&cfg.MeshSyntheticRoutesJSON, "mesh-synthetic-routes-json", getEnv(env, "RAP_MESH_SYNTHETIC_ROUTES_JSON", ""), "JSON array of synthetic mesh routes for test-only runtime.") fs.BoolVar(&cfg.RemoteWorkspaceRealAdapterEnabled, "remote-workspace-real-adapter-enabled", getEnvBool(env, "RAP_REMOTE_WORKSPACE_REAL_ADAPTER_ENABLED", false), "Request future real remote workspace adapter supervision. Disabled until the real runtime stage is implemented.") fs.StringVar(&cfg.RemoteWorkspaceRealAdapterCommand, "remote-workspace-real-adapter-command", getEnv(env, "RAP_REMOTE_WORKSPACE_REAL_ADAPTER_COMMAND", ""), "Future real remote workspace adapter command path. Redacted from status payloads.") fs.StringVar(&cfg.RemoteWorkspaceRealAdapterArgsJSON, "remote-workspace-real-adapter-args-json", getEnv(env, "RAP_REMOTE_WORKSPACE_REAL_ADAPTER_ARGS_JSON", ""), "Future real remote workspace adapter args JSON. Redacted from status payloads.") fs.StringVar(&cfg.RemoteWorkspaceRealAdapterWorkDir, "remote-workspace-real-adapter-workdir", getEnv(env, "RAP_REMOTE_WORKSPACE_REAL_ADAPTER_WORKDIR", ""), "Future real remote workspace adapter working directory. Redacted from status payloads.") heartbeatSeconds := getEnvInt(env, "RAP_HEARTBEAT_INTERVAL_SECONDS", 15) fs.DurationVar(&cfg.HeartbeatInterval, "heartbeat-interval", time.Duration(heartbeatSeconds)*time.Second, "Heartbeat interval.") enrollmentPollIntervalSeconds := getEnvInt(env, "RAP_ENROLLMENT_POLL_INTERVAL_SECONDS", 5) enrollmentPollTimeoutSeconds := getEnvSignedInt(env, "RAP_ENROLLMENT_POLL_TIMEOUT_SECONDS", 0) fs.DurationVar(&cfg.EnrollmentPollInterval, "enrollment-poll-interval", time.Duration(enrollmentPollIntervalSeconds)*time.Second, "Enrollment approval polling interval.") fs.DurationVar(&cfg.EnrollmentPollTimeout, "enrollment-poll-timeout", time.Duration(enrollmentPollTimeoutSeconds)*time.Second, "Enrollment approval polling timeout.") if err := fs.Parse(args); err != nil { return Config{}, err } cfg.ClusterID = strings.TrimSpace(cfg.ClusterID) cfg.ClusterAuthorityPublicKey = strings.TrimSpace(cfg.ClusterAuthorityPublicKey) cfg.ClusterAuthorityFingerprint = strings.TrimSpace(cfg.ClusterAuthorityFingerprint) cfg.JoinToken = strings.TrimSpace(cfg.JoinToken) cfg.NodeName = strings.TrimSpace(cfg.NodeName) cfg.StateDir = strings.TrimSpace(cfg.StateDir) cfg.FabricListenAddr = strings.TrimSpace(cfg.FabricListenAddr) cfg.MeshQUICFabricListenAddr = strings.TrimSpace(cfg.MeshQUICFabricListenAddr) cfg.FabricListenPortMode = strings.ToLower(strings.TrimSpace(cfg.FabricListenPortMode)) if cfg.VPNFabricSessionStreamShards <= 0 { cfg.VPNFabricSessionStreamShards = 8 } if cfg.VPNFabricSessionStreamShards > 128 { cfg.VPNFabricSessionStreamShards = 128 } if cfg.VPNFabricQUICMaxStreamsPerConn <= 0 { cfg.VPNFabricQUICMaxStreamsPerConn = 64 } if cfg.VPNFabricQUICIdleTTL <= 0 { cfg.VPNFabricQUICIdleTTL = 5 * time.Minute } cfg.MeshAdvertiseEndpoint = strings.TrimRight(strings.TrimSpace(cfg.MeshAdvertiseEndpoint), "/") cfg.MeshAdvertiseEndpointsJSON = strings.TrimSpace(cfg.MeshAdvertiseEndpointsJSON) cfg.FabricRegistryRecordsJSON = strings.TrimSpace(cfg.FabricRegistryRecordsJSON) cfg.MeshAdvertiseTransport = strings.TrimSpace(cfg.MeshAdvertiseTransport) if cfg.MeshAdvertiseTransport == "" { cfg.MeshAdvertiseTransport = "quic" } cfg.MeshConnectivityMode = strings.TrimSpace(cfg.MeshConnectivityMode) cfg.MeshNATType = strings.TrimSpace(cfg.MeshNATType) cfg.MeshSiteID = strings.TrimSpace(cfg.MeshSiteID) cfg.MeshLocalityGroupID = strings.TrimSpace(cfg.MeshLocalityGroupID) cfg.MeshNATGroupID = strings.TrimSpace(cfg.MeshNATGroupID) cfg.MeshSTUNReflexiveEndpoint = strings.TrimRight(strings.TrimSpace(cfg.MeshSTUNReflexiveEndpoint), "/") cfg.MeshSTUNServer = strings.TrimSpace(cfg.MeshSTUNServer) cfg.MeshRelayNodeID = strings.TrimSpace(cfg.MeshRelayNodeID) cfg.MeshRelayEndpoint = strings.TrimRight(strings.TrimSpace(cfg.MeshRelayEndpoint), "/") cfg.MeshRegion = strings.TrimSpace(cfg.MeshRegion) cfg.MeshSyntheticConfigPath = strings.TrimSpace(cfg.MeshSyntheticConfigPath) cfg.MeshPeerEndpointsJSON = strings.TrimSpace(cfg.MeshPeerEndpointsJSON) cfg.MeshSyntheticRoutesJSON = strings.TrimSpace(cfg.MeshSyntheticRoutesJSON) cfg.WebIngressSigningPrivateKey = strings.TrimSpace(cfg.WebIngressSigningPrivateKey) cfg.WebIngressSigningKeyID = strings.TrimSpace(cfg.WebIngressSigningKeyID) cfg.WebIngressTrustedKeysJSON = strings.TrimSpace(cfg.WebIngressTrustedKeysJSON) cfg.WebIngressRuntimeServiceClasses = strings.TrimSpace(cfg.WebIngressRuntimeServiceClasses) cfg.RemoteWorkspaceRealAdapterCommand = strings.TrimSpace(cfg.RemoteWorkspaceRealAdapterCommand) cfg.RemoteWorkspaceRealAdapterArgsJSON = strings.TrimSpace(cfg.RemoteWorkspaceRealAdapterArgsJSON) cfg.RemoteWorkspaceRealAdapterWorkDir = strings.TrimSpace(cfg.RemoteWorkspaceRealAdapterWorkDir) if cfg.FabricRegistryRecordsJSON == "" { return Config{}, errors.New("fabric registry records are required") } if cfg.NodeName == "" { return Config{}, errors.New("node name is required") } if cfg.StateDir == "" { return Config{}, errors.New("state dir is required") } if cfg.HeartbeatInterval <= 0 { return Config{}, errors.New("heartbeat interval must be positive") } if cfg.EnrollmentPollInterval <= 0 { return Config{}, errors.New("enrollment poll interval must be positive") } if cfg.EnrollmentPollTimeout < 0 { return Config{}, errors.New("enrollment poll timeout must not be negative") } if cfg.MeshProductionObservationSinkCapacity < 0 { return Config{}, errors.New("mesh production observation sink capacity must not be negative") } if cfg.MeshProductionObservationSinkCapacity > MaxMeshProductionObservationSinkCapacity { return Config{}, errors.New("mesh production observation sink capacity exceeds maximum") } if cfg.FabricRegistryRecordsJSON != "" && !isJSONArray(cfg.FabricRegistryRecordsJSON) { return Config{}, errors.New("fabric registry records must be a JSON array") } switch cfg.FabricListenPortMode { case "", "manual", "auto", "disabled": if cfg.FabricListenPortMode == "" { cfg.FabricListenPortMode = "manual" } default: return Config{}, errors.New("fabric listen port mode must be manual, auto, or disabled") } if cfg.FabricListenAutoPortStart <= 0 || cfg.FabricListenAutoPortEnd <= 0 { return Config{}, errors.New("fabric listen auto port range must be positive") } if cfg.FabricListenAutoPortStart > cfg.FabricListenAutoPortEnd { return Config{}, errors.New("fabric listen auto port start must be less than or equal to end") } if !isQUICAdvertiseTransport(cfg.MeshAdvertiseTransport) { return Config{}, errors.New("mesh advertise transport must be a QUIC transport label") } if hasUnsupportedEndpointScheme(cfg.MeshAdvertiseEndpoint) { return Config{}, errors.New("mesh advertise endpoint must be a QUIC endpoint") } if cfg.MeshSTUNReflexiveEndpoint != "" && hasUnsupportedEndpointScheme(cfg.MeshSTUNReflexiveEndpoint) { return Config{}, errors.New("mesh STUN reflexive endpoint must be a QUIC endpoint") } if cfg.MeshRelayEndpoint != "" && hasUnsupportedEndpointScheme(cfg.MeshRelayEndpoint) { return Config{}, errors.New("mesh relay endpoint must be a QUIC endpoint") } return cfg, nil } func isQUICAdvertiseTransport(label string) bool { switch strings.ToLower(strings.TrimSpace(label)) { case "quic", "direct_quic", "udp_quic", "quic_udp", "lan_quic", "reverse_quic", "relay_quic", "ice_quic": return true default: return false } } func hasUnsupportedEndpointScheme(endpoint string) bool { endpoint = strings.ToLower(strings.TrimSpace(endpoint)) if endpoint == "" || !strings.Contains(endpoint, "://") { return false } return !strings.HasPrefix(endpoint, "quic://") } func isJSONArray(value string) bool { var items []json.RawMessage return json.Unmarshal([]byte(strings.TrimSpace(value)), &items) == nil } func readEnv() map[string]string { out := map[string]string{} for _, pair := range os.Environ() { key, value, ok := strings.Cut(pair, "=") if ok { out[key] = value } } return out } func getEnv(env map[string]string, key, fallback string) string { if value := strings.TrimSpace(env[key]); value != "" { return value } return fallback } func getEnvInt(env map[string]string, key string, fallback int) int { value := strings.TrimSpace(env[key]) if value == "" { return fallback } parsed, err := strconv.Atoi(value) if err != nil || parsed <= 0 { return fallback } return parsed } func getEnvSignedInt(env map[string]string, key string, fallback int) int { value := strings.TrimSpace(env[key]) if value == "" { return fallback } parsed, err := strconv.Atoi(value) if err != nil { return fallback } return parsed } func getEnvBool(env map[string]string, key string, fallback bool) bool { value := strings.ToLower(strings.TrimSpace(env[key])) switch value { case "1", "true", "yes", "y", "on": return true case "0", "false", "no", "n", "off": return false default: return fallback } } func hostnameOrDefault() string { host, err := os.Hostname() if err != nil || strings.TrimSpace(host) == "" { return "rap-node" } return host }