feat: ship agent-first mission control v0.2.57
This commit is contained in:
@@ -0,0 +1,585 @@
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ModelContextProtocol.Server;
|
||||
using Nexus.Api.Controllers;
|
||||
using Nexus.Api.Data;
|
||||
using Nexus.Api.Models;
|
||||
using Nexus.Api.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace Nexus.Api.Tests;
|
||||
|
||||
public sealed class AgentProposalServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Create_IsIdempotent_AndNeverMutatesOpenClaw()
|
||||
{
|
||||
await using var fixture = await AgentProposalFixture.CreateAsync(
|
||||
externalIdentitySupported: false);
|
||||
var request = Proposal("Release Analyst");
|
||||
var invocation = Invocation("proposal-1");
|
||||
|
||||
var first = await fixture.Service.CreateAsync(
|
||||
request,
|
||||
"manual",
|
||||
invocation);
|
||||
var replay = await fixture.Service.CreateAsync(
|
||||
request,
|
||||
"manual",
|
||||
invocation);
|
||||
|
||||
Assert.True(first.Ok);
|
||||
Assert.Equal(AgentProposalStates.AwaitingApproval, first.State);
|
||||
Assert.True(replay.Ok);
|
||||
Assert.Equal("idempotent_replay", replay.State);
|
||||
Assert.Equal(first.Proposal!.Id, replay.Proposal!.Id);
|
||||
Assert.Equal(1, await fixture.Db.AgentProposals.CountAsync());
|
||||
Assert.Equal(0, fixture.Gateway.CreateCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListCursor_DoesNotSkipProposalsWithTheSameCreatedAt()
|
||||
{
|
||||
await using var fixture = await AgentProposalFixture.CreateAsync(
|
||||
externalIdentitySupported: false);
|
||||
foreach (var suffix in new[] { "a", "b", "c" })
|
||||
{
|
||||
await fixture.Service.CreateAsync(
|
||||
Proposal($"Release Analyst {suffix}") with
|
||||
{
|
||||
ClientRequestId = $"proposal-{suffix}"
|
||||
},
|
||||
"manual",
|
||||
Invocation($"proposal-{suffix}"));
|
||||
}
|
||||
|
||||
var timestamp = new DateTimeOffset(
|
||||
2026,
|
||||
7,
|
||||
30,
|
||||
12,
|
||||
0,
|
||||
0,
|
||||
TimeSpan.Zero);
|
||||
foreach (var proposal in await fixture.Db.AgentProposals.ToListAsync())
|
||||
{
|
||||
proposal.CreatedAt = timestamp;
|
||||
proposal.UpdatedAt = timestamp;
|
||||
}
|
||||
await fixture.Db.SaveChangesAsync();
|
||||
|
||||
var first = await fixture.Service.GetAsync(limit: 2);
|
||||
var second = await fixture.Service.GetAsync(
|
||||
limit: 2,
|
||||
cursor: first.NextCursor);
|
||||
|
||||
Assert.Equal(2, first.Items.Count);
|
||||
Assert.Single(second.Items);
|
||||
Assert.Null(second.NextCursor);
|
||||
Assert.Equal(
|
||||
3,
|
||||
first.Items
|
||||
.Concat(second.Items)
|
||||
.Select(item => item.Id)
|
||||
.Distinct()
|
||||
.Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Approve_FailsClosed_WhenExternalClientIdentityIsUnsupported()
|
||||
{
|
||||
await using var fixture = await AgentProposalFixture.CreateAsync(
|
||||
externalIdentitySupported: false);
|
||||
var created = await fixture.Service.CreateAsync(
|
||||
Proposal("Release Analyst"),
|
||||
"manual",
|
||||
Invocation("proposal-1"));
|
||||
|
||||
var approved = await fixture.Service.ApproveAsync(
|
||||
created.Proposal!.Id,
|
||||
new AgentProposalActionRequest(created.Proposal.Revision),
|
||||
Invocation("approve-1"));
|
||||
|
||||
Assert.False(approved.Ok);
|
||||
Assert.Equal("experimental_blocked", approved.State);
|
||||
Assert.Equal(0, await fixture.Db.AgentProvisionRequests.CountAsync());
|
||||
Assert.Equal(0, fixture.Gateway.CreateCalls);
|
||||
Assert.Equal(
|
||||
AgentProposalStates.AwaitingApproval,
|
||||
(await fixture.Service.GetByIdAsync(created.Proposal.Id))!.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApprovedProposal_CreatesOnce_VerifiesInventory_AndFinalizesFiles()
|
||||
{
|
||||
await using var fixture = await AgentProposalFixture.CreateAsync(
|
||||
externalIdentitySupported: true);
|
||||
var created = await fixture.Service.CreateAsync(
|
||||
Proposal("Release Analyst"),
|
||||
"manual",
|
||||
Invocation("proposal-1"));
|
||||
var approved = await fixture.Service.ApproveAsync(
|
||||
created.Proposal!.Id,
|
||||
new AgentProposalActionRequest(created.Proposal.Revision),
|
||||
Invocation("approve-1"));
|
||||
|
||||
Assert.True(approved.Ok);
|
||||
Assert.Equal(AgentProposalStates.Provisioning, approved.State);
|
||||
Assert.True(await fixture.Service.ProcessNextAsync());
|
||||
|
||||
var completed = await fixture.Service.GetByIdAsync(created.Proposal.Id);
|
||||
Assert.NotNull(completed);
|
||||
Assert.Equal(AgentProposalStates.Ready, completed!.Status);
|
||||
Assert.Equal("release-analyst", completed.OpenClawAgentId);
|
||||
Assert.Equal(1, fixture.Gateway.CreateCalls);
|
||||
Assert.Contains(
|
||||
"release-analyst/IDENTITY.md",
|
||||
fixture.AgentFiles.Writes.Keys);
|
||||
Assert.Contains(
|
||||
"release-analyst/AGENTS.md",
|
||||
fixture.AgentFiles.Writes.Keys);
|
||||
Assert.Empty(completed.Files);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Provisioning_ResolvesWorkspaceFromLiveOpenClawDefaults()
|
||||
{
|
||||
await using var fixture = await AgentProposalFixture.CreateAsync(
|
||||
externalIdentitySupported: true);
|
||||
var created = await fixture.Service.CreateAsync(
|
||||
Proposal("Release Analyst"),
|
||||
"manual",
|
||||
Invocation("proposal-live-workspace"));
|
||||
fixture.AgentFiles.DefaultWorkspace = "/srv/openclaw/workspaces";
|
||||
|
||||
await fixture.Service.ApproveAsync(
|
||||
created.Proposal!.Id,
|
||||
new AgentProposalActionRequest(created.Proposal.Revision),
|
||||
Invocation("approve-live-workspace"));
|
||||
Assert.True(await fixture.Service.ProcessNextAsync());
|
||||
|
||||
var completed = await fixture.Service.GetByIdAsync(created.Proposal.Id);
|
||||
Assert.Equal(
|
||||
"/srv/openclaw/workspaces/release-analyst",
|
||||
completed!.Workspace);
|
||||
Assert.Equal(
|
||||
"/srv/openclaw/workspaces/release-analyst",
|
||||
completed.OpenClawWorkspace);
|
||||
Assert.Equal(1, fixture.Gateway.CreateCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UncertainCreate_IsNotRepeated_AndRetryReconcilesBeforeAnotherCreate()
|
||||
{
|
||||
await using var fixture = await AgentProposalFixture.CreateAsync(
|
||||
externalIdentitySupported: true);
|
||||
fixture.Gateway.ThrowUncertainCreate = true;
|
||||
var created = await fixture.Service.CreateAsync(
|
||||
Proposal("Release Analyst"),
|
||||
"manual",
|
||||
Invocation("proposal-1"));
|
||||
await fixture.Service.ApproveAsync(
|
||||
created.Proposal!.Id,
|
||||
new AgentProposalActionRequest(created.Proposal.Revision),
|
||||
Invocation("approve-1"));
|
||||
|
||||
Assert.True(await fixture.Service.ProcessNextAsync());
|
||||
Assert.False(await fixture.Service.ProcessNextAsync());
|
||||
var uncertain = await fixture.Service.GetByIdAsync(created.Proposal.Id);
|
||||
Assert.Equal(AgentProposalStates.InDoubt, uncertain!.Status);
|
||||
Assert.Equal(1, fixture.Gateway.CreateCalls);
|
||||
|
||||
var retry = await fixture.Service.RetryAsync(
|
||||
created.Proposal.Id,
|
||||
new AgentProposalActionRequest(uncertain.Revision),
|
||||
Invocation("retry-1"));
|
||||
Assert.True(retry.Ok);
|
||||
Assert.True(await fixture.Service.ProcessNextAsync());
|
||||
|
||||
var reconciled = await fixture.Service.GetByIdAsync(created.Proposal.Id);
|
||||
Assert.Equal(AgentProposalStates.Failed, reconciled!.Status);
|
||||
Assert.Equal("reconciled_absent", reconciled.Error!.Code);
|
||||
Assert.Equal(1, fixture.Gateway.CreateCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExpiredCreateLease_IsReclaimedAsReadOnlyReconciliation()
|
||||
{
|
||||
await using var fixture = await AgentProposalFixture.CreateAsync(
|
||||
externalIdentitySupported: true);
|
||||
var created = await fixture.Service.CreateAsync(
|
||||
Proposal("Lease Sentinel"),
|
||||
"manual",
|
||||
Invocation("proposal-lease"));
|
||||
await fixture.Service.ApproveAsync(
|
||||
created.Proposal!.Id,
|
||||
new AgentProposalActionRequest(created.Proposal.Revision),
|
||||
Invocation("approve-lease"));
|
||||
|
||||
var request = await fixture.Db.AgentProvisionRequests.SingleAsync();
|
||||
request.Status = AgentProvisionRequestStates.Dispatching;
|
||||
request.DispatchStartedAt = DateTimeOffset.UtcNow.AddMinutes(-5);
|
||||
request.LeaseOwner = "abandoned-worker";
|
||||
request.LeaseUntil = DateTimeOffset.UtcNow.AddSeconds(-1);
|
||||
await fixture.Db.SaveChangesAsync();
|
||||
|
||||
Assert.True(await fixture.Service.ProcessNextAsync());
|
||||
|
||||
var reconciled = await fixture.Service.GetByIdAsync(
|
||||
created.Proposal.Id);
|
||||
Assert.Equal(AgentProposalStates.Failed, reconciled!.Status);
|
||||
Assert.Equal("reconciled_absent", reconciled.Error!.Code);
|
||||
Assert.Equal(0, fixture.Gateway.CreateCalls);
|
||||
Assert.Contains(
|
||||
await fixture.Db.OutboxEvents.ToListAsync(),
|
||||
item => item.Type == "agent.provision.lease_reclaimed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Controller_AndMcpTools_ExposeExpectedSecurityMetadata()
|
||||
{
|
||||
var authorize = typeof(OpenClawAgentProposalsController)
|
||||
.GetCustomAttributes(typeof(AuthorizeAttribute), inherit: true)
|
||||
.OfType<AuthorizeAttribute>()
|
||||
.Single();
|
||||
Assert.Equal("owner", authorize.Roles);
|
||||
|
||||
var propose = typeof(NexusMcpTools).GetMethod(
|
||||
nameof(NexusMcpTools.ProposeAgent))!;
|
||||
var proposeTool = propose.GetCustomAttributes(
|
||||
typeof(McpServerToolAttribute),
|
||||
inherit: false)
|
||||
.OfType<McpServerToolAttribute>()
|
||||
.Single();
|
||||
Assert.Equal("nexus_propose_agent", proposeTool.Name);
|
||||
Assert.False(proposeTool.ReadOnly);
|
||||
Assert.False(proposeTool.Destructive);
|
||||
Assert.True(proposeTool.Idempotent);
|
||||
Assert.False(proposeTool.OpenWorld);
|
||||
Assert.True(proposeTool.UseStructuredContent);
|
||||
Assert.Equal(typeof(AgentProposalToolResult), proposeTool.OutputSchemaType);
|
||||
|
||||
var get = typeof(NexusMcpTools).GetMethod(
|
||||
nameof(NexusMcpTools.GetAgentProposal))!;
|
||||
var getTool = get.GetCustomAttributes(
|
||||
typeof(McpServerToolAttribute),
|
||||
inherit: false)
|
||||
.OfType<McpServerToolAttribute>()
|
||||
.Single();
|
||||
Assert.True(getTool.ReadOnly);
|
||||
Assert.False(getTool.Destructive);
|
||||
}
|
||||
|
||||
private static CreateAgentProposalRequest Proposal(string name)
|
||||
=> new(
|
||||
name,
|
||||
Role: "Release quality",
|
||||
Description: "Verify releases and report evidence.",
|
||||
Model: "openai/gpt-5.5",
|
||||
ClientRequestId: "proposal-1");
|
||||
|
||||
private static OpenClawInvocationMetadata Invocation(string key)
|
||||
=> new(key, $"correlation-{key}", "bao", null);
|
||||
}
|
||||
|
||||
internal sealed class AgentProposalFixture : IAsyncDisposable
|
||||
{
|
||||
private AgentProposalFixture(
|
||||
NexusDbContext db,
|
||||
AgentProposalService service,
|
||||
ProposalGatewayConnector gateway,
|
||||
ProposalAgentConfigurationService agentFiles)
|
||||
{
|
||||
Db = db;
|
||||
Service = service;
|
||||
Gateway = gateway;
|
||||
AgentFiles = agentFiles;
|
||||
}
|
||||
|
||||
public NexusDbContext Db { get; }
|
||||
public AgentProposalService Service { get; }
|
||||
public ProposalGatewayConnector Gateway { get; }
|
||||
public ProposalAgentConfigurationService AgentFiles { get; }
|
||||
|
||||
public static async Task<AgentProposalFixture> CreateAsync(
|
||||
bool externalIdentitySupported)
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<NexusDbContext>()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
var db = new NexusDbContext(options);
|
||||
await db.Database.EnsureCreatedAsync();
|
||||
|
||||
var gateway = new ProposalGatewayConnector();
|
||||
var management = new OpenClawManagementState();
|
||||
management.SetEnabled(true);
|
||||
db.OpenClawConnectionProfiles.Add(new OpenClawConnectionProfile
|
||||
{
|
||||
Endpoint = "ws://127.0.0.1:18789/",
|
||||
DiscoverySource = "test",
|
||||
RequiredVersion = gateway.RequiredVersion,
|
||||
AdoptionState = OpenClawAdoptionStates.Adopted,
|
||||
ManagementEnabled = true,
|
||||
CapabilityHash = AgentProposalService.BuildCapabilityHash(gateway),
|
||||
Revision = 1
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["OpenClawSetup:ExternalClientIdentitySupported"] =
|
||||
externalIdentitySupported.ToString()
|
||||
})
|
||||
.Build();
|
||||
var agentFiles = new ProposalAgentConfigurationService();
|
||||
var service = new AgentProposalService(
|
||||
db,
|
||||
gateway,
|
||||
agentFiles,
|
||||
new StubOpenClawWriteGate(
|
||||
externalIdentitySupported
|
||||
? OpenClawWriteGateDecision.Permit()
|
||||
: OpenClawWriteGateDecision.Block(
|
||||
"experimental_blocked",
|
||||
"External identity is not supported.")),
|
||||
Options.Create(new AgentProvisioningOptions()),
|
||||
new AgentProvisioningSignal(),
|
||||
NullLogger<AgentProposalService>.Instance);
|
||||
return new AgentProposalFixture(db, service, gateway, agentFiles);
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync() => Db.DisposeAsync();
|
||||
}
|
||||
|
||||
internal sealed class ProposalGatewayConnector : IGatewayConnector
|
||||
{
|
||||
private readonly List<(string Id, string Workspace)> agents = [];
|
||||
|
||||
public bool ThrowUncertainCreate { get; set; }
|
||||
public int CreateCalls { get; private set; }
|
||||
public GatewayConnectionState ConnectionState => GatewayConnectionState.Connected;
|
||||
public string? GatewayVersion => "2026.7.1";
|
||||
public string? RequiredVersion => "2026.7.1";
|
||||
public DateTimeOffset? LastConnectedAt => DateTimeOffset.UtcNow;
|
||||
public int ReconnectAttempts => 0;
|
||||
public string? StatusMessage => "ready";
|
||||
public string? DeviceId => "test-device";
|
||||
public bool DeviceTokenConfigured => true;
|
||||
public bool PairingRequired => false;
|
||||
public string? PairingRequestId => null;
|
||||
public int? ProtocolVersion => 4;
|
||||
public IReadOnlySet<string> AdvertisedMethods { get; } = new HashSet<string>(
|
||||
[
|
||||
"agents.list",
|
||||
"agents.create",
|
||||
"agents.files.get",
|
||||
"agents.files.set",
|
||||
"config.get",
|
||||
"models.list"
|
||||
],
|
||||
StringComparer.Ordinal);
|
||||
public IReadOnlySet<string> AdvertisedEvents { get; } =
|
||||
new HashSet<string>(StringComparer.Ordinal);
|
||||
public IReadOnlySet<string> GrantedScopes { get; } = new HashSet<string>(
|
||||
["operator.read", "operator.admin"],
|
||||
StringComparer.Ordinal);
|
||||
public DateTimeOffset? LastEventAt => null;
|
||||
|
||||
public bool Supports(string method) => AdvertisedMethods.Contains(method);
|
||||
|
||||
public Task<JsonNode?> InvokeAsync(
|
||||
string method,
|
||||
object? parameters = null,
|
||||
TimeSpan? timeout = null,
|
||||
CancellationToken cancellationToken = default,
|
||||
OpenClawInvocationContext? invocationContext = null)
|
||||
{
|
||||
if (method == "agents.list")
|
||||
{
|
||||
return Task.FromResult<JsonNode?>(new JsonObject
|
||||
{
|
||||
["agents"] = new JsonArray(agents
|
||||
.Select(item => (JsonNode)new JsonObject
|
||||
{
|
||||
["id"] = item.Id,
|
||||
["workspace"] = item.Workspace
|
||||
})
|
||||
.ToArray())
|
||||
});
|
||||
}
|
||||
|
||||
if (method == "models.list")
|
||||
{
|
||||
return Task.FromResult<JsonNode?>(new JsonObject
|
||||
{
|
||||
["models"] = new JsonArray
|
||||
{
|
||||
new JsonObject
|
||||
{
|
||||
["id"] = "openai/gpt-5.5",
|
||||
["name"] = "GPT-5.5",
|
||||
["provider"] = "openai",
|
||||
["available"] = true
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (method == "agents.create")
|
||||
{
|
||||
CreateCalls++;
|
||||
if (ThrowUncertainCreate)
|
||||
{
|
||||
throw new OpenClawGatewayRpcException(
|
||||
"GATEWAY_DISCONNECTED",
|
||||
"Connection dropped.",
|
||||
retryable: true);
|
||||
}
|
||||
|
||||
var json = JsonSerializer.SerializeToNode(parameters)!.AsObject();
|
||||
var name = json["name"]!.GetValue<string>();
|
||||
var id = name.Trim().ToLowerInvariant().Replace(' ', '-');
|
||||
var workspace = json["workspace"]!.GetValue<string>();
|
||||
agents.Add((id, workspace));
|
||||
return Task.FromResult<JsonNode?>(new JsonObject
|
||||
{
|
||||
["ok"] = true,
|
||||
["agentId"] = id,
|
||||
["name"] = name,
|
||||
["workspace"] = workspace
|
||||
});
|
||||
}
|
||||
|
||||
throw new OpenClawGatewayRpcException(
|
||||
"METHOD_NOT_FOUND",
|
||||
$"Unexpected method {method}.");
|
||||
}
|
||||
|
||||
public IReadOnlyList<GatewayEventEnvelope> GetRecentEvents(int limit = 100)
|
||||
=> [];
|
||||
}
|
||||
|
||||
internal sealed class ProposalAgentConfigurationService
|
||||
: IOpenClawAgentConfigurationService
|
||||
{
|
||||
public Dictionary<string, string> Writes { get; } =
|
||||
new(StringComparer.Ordinal);
|
||||
public string? DefaultWorkspace { get; set; }
|
||||
|
||||
public Task<OpenClawAgentFileDto> GetAgentFileAsync(
|
||||
string agentId,
|
||||
string fileName,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var key = $"{agentId}/{fileName}";
|
||||
if (Writes.TryGetValue(key, out var content))
|
||||
{
|
||||
return Task.FromResult(File(agentId, fileName, content));
|
||||
}
|
||||
|
||||
return Task.FromResult(new OpenClawAgentFileDto(
|
||||
agentId,
|
||||
fileName,
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
OpenClawAgentConfigurationService.MissingContentHash,
|
||||
DateTimeOffset.UtcNow));
|
||||
}
|
||||
|
||||
public async Task<OpenClawAgentFileWriteDto> SetAgentFileAsync(
|
||||
string agentId,
|
||||
string fileName,
|
||||
UpdateOpenClawAgentFileRequest request,
|
||||
OpenClawInvocationContext invocationContext,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Writes[$"{agentId}/{fileName}"] = request.Content;
|
||||
var file = await GetAgentFileAsync(agentId, fileName, cancellationToken);
|
||||
return new OpenClawAgentFileWriteDto(
|
||||
true,
|
||||
"completed",
|
||||
"verified",
|
||||
file,
|
||||
true,
|
||||
invocationContext.IdempotencyKey,
|
||||
invocationContext.CorrelationId,
|
||||
DateTimeOffset.UtcNow);
|
||||
}
|
||||
|
||||
public Task<OpenClawAgentFileCollectionDto> GetAgentFilesAsync(
|
||||
string agentId,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public Task<OpenClawWorkspaceCollectionDto> GetWorkspaceAsync(
|
||||
string agentId,
|
||||
string? path,
|
||||
int offset,
|
||||
int limit,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public Task<OpenClawWorkspaceFileDto> GetWorkspaceFileAsync(
|
||||
string agentId,
|
||||
string path,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public Task<OpenClawConfigSchemaLookupDto> GetConfigSchemaAsync(
|
||||
string path,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public Task<OpenClawConfigSnapshotDto> GetConfigAsync(
|
||||
CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(new OpenClawConfigSnapshotDto(
|
||||
true,
|
||||
true,
|
||||
"config-hash",
|
||||
new JsonObject
|
||||
{
|
||||
["agents"] = new JsonObject
|
||||
{
|
||||
["defaults"] = new JsonObject
|
||||
{
|
||||
["workspace"] = DefaultWorkspace
|
||||
}
|
||||
}
|
||||
},
|
||||
null,
|
||||
null,
|
||||
DateTimeOffset.UtcNow));
|
||||
|
||||
public Task<OpenClawConfigPatchDto> PatchConfigAsync(
|
||||
PatchOpenClawConfigRequest request,
|
||||
OpenClawInvocationContext invocationContext,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
private static OpenClawAgentFileDto File(
|
||||
string agentId,
|
||||
string name,
|
||||
string content)
|
||||
=> new(
|
||||
agentId,
|
||||
name,
|
||||
false,
|
||||
Encoding.UTF8.GetByteCount(content),
|
||||
DateTimeOffset.UtcNow,
|
||||
content,
|
||||
Convert.ToHexString(
|
||||
SHA256.HashData(Encoding.UTF8.GetBytes(content)))
|
||||
.ToLowerInvariant(),
|
||||
DateTimeOffset.UtcNow);
|
||||
}
|
||||
Reference in New Issue
Block a user