agent: unify listening and dialing with directed mutual measurements

Replace Probe with Agent across node runtime, Fleet contracts, backend state, API and storage. Report incoming sessions and outgoing link health through each Agent uplink, and retain directed link controls and retry.

Update protocol fixtures and end-to-end coverage for the unified model; accept requested cancellation during the Fleet timeout test cleanup.
This commit is contained in:
justme committed 2026-10-21 11:55:36 +08:00
1 parent d5ebf29e57
commit ad5efc62ff
206 files changed
+4202 -4964

No files matched your search

@@ -0,0 +1,35 @@
using System.Text.Json;
using Fleet.Rpc;
using Fleet.Server;
using MoShiro.Backend.Application;
using MoShiro.Protocol;
namespace MoShiro.Backend.Gateway;
/// <summary>期望状态:为每个节点下发它要拨出的监听节点列表(地址、端口、证书公钥)。节点集合或线路变化时由 <c>NotifyAllChanged</c> 一起重算推送。</summary>
public sealed class DialTargetsStateProvider : IStateProvider
{
private readonly AgentService _agents;
private readonly IAgentStore _store;
public DialTargetsStateProvider(AgentService agents, IAgentStore store)
{
_agents = agents;
_store = store;
}
public string Type => DialTargets.StateType;
public async ValueTask<StateDocument?> GetAsync(NodeRecord node, CancellationToken ct)
{
if (await _store.GetAsync(node.Id, ct) is not { Status: NodeStatus.Active } dialer)
{
return null;
}
DialTargets targets = await _agents.DialTargetsAsync(dialer, ct);
return new StateDocument(targets.Version, JsonSerializer.SerializeToElement(targets, MoShiroJsonContext.Default.DialTargets));
}
public ValueTask OnAppliedAsync(NodeRecord node, long version, StateSyncResult result, CancellationToken ct) => ValueTask.CompletedTask;
}
@@ -50,10 +50,10 @@ public sealed class FleetGateway : IAgentGateway
await Connections.RequestAsync(
agent, MoShiroMethods.TestCancel, new TestCancelParams { TestId = test }, MoShiroJsonContext.Default.TestCancelParams, MoShiroJsonContext.Default.TestRunResult, ct);
public async ValueTask<Result<bool>> DisconnectProbeAsync(NodeId agent, ProbeId probe, CancellationToken ct)
public async ValueTask<Result<bool>> DisconnectDialerAsync(NodeId listener, NodeId dialer, CancellationToken ct)
{
Result<TestRunResult> result = await Connections.RequestAsync(
agent, MoShiroMethods.ProbeDisconnect, new ProbeDisconnectParams { ProbeId = probe }, MoShiroJsonContext.Default.ProbeDisconnectParams, MoShiroJsonContext.Default.TestRunResult, ct);
listener, MoShiroMethods.DialerDisconnect, new DialerDisconnectParams { DialerId = dialer }, MoShiroJsonContext.Default.DialerDisconnectParams, MoShiroJsonContext.Default.TestRunResult, ct);
if (!result.TryGet(out TestRunResult? done, out FleetError? error))
{
return Result<bool>.Fail(error.Code == FleetErrors.NodeOffline ? MoShiroErrors.AgentOffline : error.Code, error.Message);
@@ -64,8 +64,8 @@ public sealed class FleetGateway : IAgentGateway
public ValueTask DisconnectAsync(NodeId agent, string reason, CancellationToken ct) => Connections.DisconnectAsync(agent, reason, ct);
public ValueTask<bool> NotifyProbeLinkRetryAsync(NodeId probe, NodeId agent, CancellationToken ct) =>
Connections.NotifyAsync(probe, MoShiroMethods.ProbeLinkRetry, new ProbeLinkRetryParams { AgentId = agent }, MoShiroJsonContext.Default.ProbeLinkRetryParams, ct);
public ValueTask<bool> NotifyLinkRetryAsync(NodeId dialer, NodeId listener, CancellationToken ct) =>
Connections.NotifyAsync(dialer, MoShiroMethods.LinkRetry, new LinkRetryParams { ListenerId = listener }, MoShiroJsonContext.Default.LinkRetryParams, ct);
public async ValueTask NotifyUpdateOfferAsync(NodeId node, string manifest, string signature, CancellationToken ct) =>
await Connections.NotifyAsync(node, MoShiroMethods.UpdateOffer, new UpdateOfferParams { Manifest = manifest, Signature = signature }, MoShiroJsonContext.Default.UpdateOfferParams, ct);
@@ -10,7 +10,7 @@ namespace MoShiro.Backend.Gateway;
public static class GatewayServiceCollectionExtensions
{
/// <summary>
/// 在 Fleet.Server 之上接入 MoShiro:期望状态(TaskSet)、RawChunk 收件箱、<c>moshiro.*</c> 方法、注册挂钩和节点事件。
/// 在 Fleet.Server 之上接入 MoShiro:期望状态(TaskSet、DialTargets)、RawChunk 收件箱、<c>moshiro.*</c> 方法、注册挂钩和节点事件。
/// 需要事先注册 <see cref="IFleetStore"/> 和 <see cref="TimeProvider"/>,并在 <c>AddMoShiroApplication</c> 之后调用。
/// </summary>
public static IServiceCollection AddMoShiroGateway(this IServiceCollection services)
@@ -18,15 +18,11 @@ public static class GatewayServiceCollectionExtensions
services.AddSingleton<IAgentGateway, FleetGateway>();
services.AddHostedService<ConnectionShutdown>();
services.AddFleetEnrollment();
services.AddFleetServer(options =>
{
options.NodeIdPrefix = AgentIds.Prefix;
options.NodeIdPrefixes[JoinTokenService.ProbeRole] = ProbeId.Prefix;
})
services.AddFleetServer(options => options.NodeIdPrefix = AgentIds.Prefix)
.AddState<TaskSetStateProvider>()
.AddState<ProbeAgentsStateProvider>()
.AddState<DialTargetsStateProvider>()
.AddInbox<RawChunkInbox>()
.WithMethods(MoShiroMethodTable.Table, node => NodeRoles.Of(node.Id.Value))
.WithMethods(MoShiroMethodTable.Table)
.MapMethods(MapMoShiroMethods);
return services;
}
@@ -1,33 +0,0 @@
using System.Text.Json;
using Fleet.Rpc;
using Fleet.Server;
using MoShiro.Backend.Application;
using MoShiro.Protocol;
namespace MoShiro.Backend.Gateway;
/// <summary>期望状态:为每个 Probe 下发要连接的 Agent 列表(地址、端口、证书指纹)。Agent 集合变化时由 <c>NotifyAllChanged</c> 一起重算推送。</summary>
public sealed class ProbeAgentsStateProvider : IStateProvider
{
private readonly AgentService _agents;
public ProbeAgentsStateProvider(AgentService agents)
{
_agents = agents;
}
public string Type => ProbeAgentList.StateType;
public async ValueTask<StateDocument?> GetAsync(NodeRecord node, CancellationToken ct)
{
if (!node.Id.Value.StartsWith(ProbeId.Prefix, StringComparison.Ordinal))
{
return null;
}
ProbeAgentList list = await _agents.ProbeAgentsAsync(node.Id.Value, ct);
return new StateDocument(list.Version, JsonSerializer.SerializeToElement(list, MoShiroJsonContext.Default.ProbeAgentList));
}
public ValueTask OnAppliedAsync(NodeRecord node, long version, StateSyncResult result, CancellationToken ct) => ValueTask.CompletedTask;
}
@@ -8,7 +8,7 @@ using MoShiro.Protocol;
namespace MoShiro.Backend.Gateway;
/// <summary>
/// 期望状态:为每个 Agent 下发完整的 <see cref="TaskSet"/> 快照(有效任务集 + Probe 公钥)。
/// 期望状态:为每个 Agent 下发完整的 <see cref="TaskSet"/> 快照(有效任务集 + 允许拨入的拨号方公钥)。
/// 节点应用之后回传 <c>applied_version</c> 和 <c>rejected[]</c>(<c>unsupported</c>、<c>budget_exceeded</c>、<c>policy_denied</c>),面板上可以看到。
/// </summary>
public sealed class TaskSetStateProvider : IStateProvider
@@ -26,11 +26,6 @@ public sealed class TaskSetStateProvider : IStateProvider
public async ValueTask<StateDocument?> GetAsync(NodeRecord node, CancellationToken ct)
{
if (node.Id.Value.StartsWith(ProbeId.Prefix, StringComparison.Ordinal))
{
return null; // Probe 不运行任务集
}
DesiredTaskSet? desired = await _planner.GetDesiredAsync(node.Id, ct);
return desired is null ? null : new StateDocument(desired.Version, JsonSerializer.SerializeToElement(desired.Document, MoShiroJsonContext.Default.TaskSet));
}