Add S3 erasure swarm with CloudFront signed magnets.
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
Operators configure bucket and CloudFront domain with env credentials; deploy plans upload RS 4+2 shards and attach signed edge URLs to BGP swarm magnets. Agents fetch LAN, CloudFront, then C2. Forge panel adds test and IAM policy JSON.
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react';
|
||||
import AwsErasureSwarmPanel from './AwsErasureSwarmPanel';
|
||||
|
||||
vi.mock('../../api/client', () => ({
|
||||
api: {
|
||||
updateConfig: vi.fn().mockResolvedValue({}),
|
||||
testErasureSwarm: vi.fn().mockResolvedValue({ ok: true }),
|
||||
getErasureSwarmPolicyJSON: vi.fn().mockResolvedValue({ iam_policy: '{}', bucket_policy: '{}', env_keys: [] }),
|
||||
},
|
||||
}));
|
||||
|
||||
import { api } from '../../api/client';
|
||||
|
||||
const baseServer = {
|
||||
public_url: '',
|
||||
stats_retention_hours: 168,
|
||||
build_retention_days: 7,
|
||||
pool_reconnect_seconds: 30,
|
||||
websocket_ping_seconds: 30,
|
||||
max_agents: 500,
|
||||
max_build_size_mb: 50,
|
||||
log_agent_connections: false,
|
||||
log_share_submissions: false,
|
||||
log_pool_traffic: false,
|
||||
strict_wallet_validation: true,
|
||||
dashboard_subtitle: '',
|
||||
open_firewall_on_start: false,
|
||||
aws_s3_shard_bucket: 'b',
|
||||
aws_cloudfront_domain: 'd.cf.net',
|
||||
};
|
||||
|
||||
describe('AwsErasureSwarmPanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('renders bucket and CloudFront inputs', () => {
|
||||
render(<AwsErasureSwarmPanel server={baseServer} onServerChange={() => {}} />);
|
||||
expect(screen.getByDisplayValue('b')).toBeTruthy();
|
||||
expect(screen.getByDisplayValue('d.cf.net')).toBeTruthy();
|
||||
expect(screen.getByText(/AWS Erasure Swarm/i)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('calls test endpoint on Test connection', async () => {
|
||||
render(<AwsErasureSwarmPanel server={baseServer} onServerChange={() => {}} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: /Test connection/i }));
|
||||
await waitFor(() => {
|
||||
expect(api.testErasureSwarm).toHaveBeenCalledWith({ s3_bucket: 'b', cloudfront_domain: 'd.cf.net' });
|
||||
});
|
||||
});
|
||||
|
||||
it('loads policy JSON on button click', async () => {
|
||||
render(<AwsErasureSwarmPanel server={baseServer} onServerChange={() => {}} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: /Generate policy JSON/i }));
|
||||
await waitFor(() => {
|
||||
expect(api.getErasureSwarmPolicyJSON).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
87
server/web/src/components/Forge/AwsErasureSwarmPanel.tsx
Normal file
87
server/web/src/components/Forge/AwsErasureSwarmPanel.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { useState } from 'react';
|
||||
import { api } from '../../api/client';
|
||||
import type { ServerSettings } from '../../types';
|
||||
import { HelpTip } from '../HelpTip';
|
||||
|
||||
type Props = {
|
||||
server: ServerSettings;
|
||||
onServerChange: (patch: Partial<ServerSettings>) => void;
|
||||
};
|
||||
|
||||
export default function AwsErasureSwarmPanel({ server, onServerChange }: Props) {
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [testMsg, setTestMsg] = useState('');
|
||||
const [policyJSON, setPolicyJSON] = useState('');
|
||||
|
||||
const bucket = server.aws_s3_shard_bucket ?? '';
|
||||
const cfDomain = server.aws_cloudfront_domain ?? '';
|
||||
|
||||
const saveFields = async (patch: Partial<ServerSettings>) => {
|
||||
onServerChange(patch);
|
||||
await api.updateConfig({ server: { ...server, ...patch } });
|
||||
};
|
||||
|
||||
const runTest = async () => {
|
||||
setTesting(true);
|
||||
setTestMsg('');
|
||||
try {
|
||||
const res = await api.testErasureSwarm({ s3_bucket: bucket, cloudfront_domain: cfDomain });
|
||||
setTestMsg(res.ok ? 'S3 head bucket OK — credentials and signing ready.' : (res.error || 'Test failed'));
|
||||
} catch (e) {
|
||||
setTestMsg(e instanceof Error ? e.message : 'Test failed');
|
||||
} finally {
|
||||
setTesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadPolicy = async () => {
|
||||
setPolicyJSON('');
|
||||
try {
|
||||
const res = await api.getErasureSwarmPolicyJSON(bucket);
|
||||
setPolicyJSON(JSON.stringify(res, null, 2));
|
||||
} catch (e) {
|
||||
setPolicyJSON(e instanceof Error ? e.message : 'Failed to load policy JSON');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="form-group card operator-deck-card" style={{ marginTop: '1rem' }}>
|
||||
<h3 className="font-tech">AWS Erasure Swarm</h3>
|
||||
<p className="form-hint">
|
||||
Upload RS 4+2 shards to S3 on deploy plans; agents fetch LAN → CloudFront → C2.
|
||||
Credentials via env: <code>AF_AWS_*</code>, <code>AF_CLOUDFRONT_*</code>.
|
||||
<HelpTip field="aws_erasure_swarm" />
|
||||
</p>
|
||||
<label className="label">S3 shard bucket</label>
|
||||
<input
|
||||
className="input"
|
||||
value={bucket}
|
||||
onChange={(e) => onServerChange({ aws_s3_shard_bucket: e.target.value })}
|
||||
onBlur={() => saveFields({ aws_s3_shard_bucket: bucket })}
|
||||
placeholder="my-fleet-shards"
|
||||
/>
|
||||
<label className="label" style={{ marginTop: '0.5rem' }}>CloudFront domain</label>
|
||||
<input
|
||||
className="input"
|
||||
value={cfDomain}
|
||||
onChange={(e) => onServerChange({ aws_cloudfront_domain: e.target.value })}
|
||||
onBlur={() => saveFields({ aws_cloudfront_domain: cfDomain })}
|
||||
placeholder="d123abc.cloudfront.net"
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '0.75rem', flexWrap: 'wrap' }}>
|
||||
<button type="button" className="btn btn-secondary" disabled={testing} onClick={runTest}>
|
||||
{testing ? 'Testing…' : 'Test connection'}
|
||||
</button>
|
||||
<button type="button" className="btn btn-secondary" onClick={loadPolicy}>
|
||||
Generate policy JSON
|
||||
</button>
|
||||
</div>
|
||||
{testMsg && <p className="form-hint" style={{ marginTop: '0.5rem' }}>{testMsg}</p>}
|
||||
{policyJSON && (
|
||||
<pre className="form-hint" style={{ marginTop: '0.5rem', maxHeight: '12rem', overflow: 'auto', fontSize: '0.75rem' }}>
|
||||
{policyJSON}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -109,6 +109,7 @@ describe('FIELD_HELP', () => {
|
||||
'failure_atlas',
|
||||
'erasure_lanes',
|
||||
'fleet_torrent',
|
||||
'aws_erasure_swarm',
|
||||
'ai_court_session',
|
||||
'ai_persona',
|
||||
'ai_persona_aggressive',
|
||||
|
||||
@@ -48,6 +48,8 @@ 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.',
|
||||
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.',
|
||||
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:
|
||||
'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:
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
type ForgeDeliverable,
|
||||
} from '../help/forgeFormNormalize';
|
||||
import { ForgeFieldBadge, ForgeLockedHint, ForgeSectionHeader } from '../components/Forge/ForgeFieldHints';
|
||||
import AwsErasureSwarmPanel from '../components/Forge/AwsErasureSwarmPanel';
|
||||
import ForgeDispenseReveal from '../components/Forge/ForgeDispenseReveal';
|
||||
import { blueprintDiff, buildRequestFromRecord } from '../help/buildManager';
|
||||
import DownloadButton from '../components/DownloadButton';
|
||||
@@ -3082,6 +3083,17 @@ export default function BuilderPage() {
|
||||
<ForgeLockedHint meta={fieldMeta.webrtc_mesh_spread} />
|
||||
</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' : ''}`}>
|
||||
<label className="checkbox-label">
|
||||
<input type="checkbox" className="checkbox" checked={!!form.com_hijack_persist}
|
||||
|
||||
Reference in New Issue
Block a user