Skip to content

Commit 2794a33

Browse files
feat(phase4): implement Tasks 4.1 and 4.2 - Pairing & Cron
Task 4.1: Pairing Protocol & Secret Store - PairingGuard: HMAC-based token exchange for device pairing - Time-limited tokens with configurable expiry - Persistent paired device storage (JSON) - Token consumption prevents reuse - SecretStore: Encrypted secret storage using AES-GCM - File-based persistence with chmod 600 on Unix - Key derivation from master key file - Supports CRUD operations for secrets Task 4.2: Cron Scheduler - ISchedule interface with three implementations: - CronScheduleExpr: Standard 5-field cron expressions via Cronos - EverySchedule: Fixed interval repetition - AtSchedule: One-shot execution at specific time - CronScheduler: SQLite-backed persistent storage - Thread-safe job management - Support for due job detection - One-shot job auto-deletion - Job enable/disable support All implementations follow TDD with 68 new tests: - 31 tests for Security (PairingGuard, SecretStore) - 37 tests for Cron (Schedules, CronScheduler)
1 parent 8b2b982 commit 2794a33

19 files changed

Lines changed: 2324 additions & 4 deletions

ClawSharp.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<Project Path="src/ClawSharp.Providers/ClawSharp.Providers.csproj" />
1111
<Project Path="src/ClawSharp.Tools/ClawSharp.Tools.csproj" />
1212
<Project Path="src/ClawSharp.UI/ClawSharp.UI.csproj" />
13+
<Project Path="src/ClawSharp.Web/ClawSharp.Web.csproj" />
1314
</Folder>
1415
<Folder Name="/tests/">
1516
<Project Path="tests/ClawSharp.Agent.Tests/ClawSharp.Agent.Tests.csproj" />

src/ClawSharp.Gateway/ClawSharp.Gateway.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<ProjectReference Include="..\ClawSharp.Channels\ClawSharp.Channels.csproj" />
1616
<ProjectReference Include="..\ClawSharp.Core\ClawSharp.Core.csproj" />
1717
<ProjectReference Include="..\ClawSharp.Infrastructure\ClawSharp.Infrastructure.csproj" />
18-
<ProjectReference Include="..\ClawSharp.UI\ClawSharp.UI.csproj" />
18+
<ProjectReference Include="..\ClawSharp.Web\ClawSharp.Web.csproj" />
1919
</ItemGroup>
2020

2121
</Project>

src/ClawSharp.Infrastructure/ClawSharp.Infrastructure.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
2020
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
2121
<PackageReference Include="Tomlyn" Version="0.20.0" />
22+
<PackageReference Include="Cronos" Version="0.9.0" />
23+
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.3" />
2224
</ItemGroup>
2325

2426
<PropertyGroup>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
namespace ClawSharp.Infrastructure.Cron;
2+
3+
/// <summary>
4+
/// One-shot schedule that fires at a specific time.
5+
/// Should be deleted after execution.
6+
/// </summary>
7+
public sealed class AtSchedule : ISchedule
8+
{
9+
private readonly DateTimeOffset _targetTime;
10+
11+
public ScheduleKind Kind => ScheduleKind.At;
12+
13+
/// <summary>Indicates this is a one-shot schedule.</summary>
14+
public bool IsOneShot => true;
15+
16+
/// <summary>The target execution time.</summary>
17+
public DateTimeOffset TargetTime => _targetTime;
18+
19+
/// <summary>
20+
/// Creates a one-shot schedule for the specified time.
21+
/// </summary>
22+
/// <param name="targetTime">The time at which to fire.</param>
23+
public AtSchedule(DateTimeOffset targetTime)
24+
{
25+
_targetTime = targetTime;
26+
}
27+
28+
/// <summary>
29+
/// Creates a one-shot schedule from a Unix timestamp in milliseconds.
30+
/// </summary>
31+
public static AtSchedule FromUnixMilliseconds(long unixMs)
32+
{
33+
return new AtSchedule(DateTimeOffset.FromUnixTimeMilliseconds(unixMs));
34+
}
35+
36+
public bool IsDue(DateTimeOffset now, DateTimeOffset? lastRun)
37+
{
38+
// If already fired, never due again
39+
if (lastRun.HasValue)
40+
return false;
41+
42+
// Due if target time has passed
43+
return now >= _targetTime;
44+
}
45+
46+
public DateTimeOffset? GetNextOccurrence(DateTimeOffset from)
47+
{
48+
// If target is in the future, return it
49+
if (_targetTime > from)
50+
return _targetTime;
51+
52+
// Target has passed, no more occurrences
53+
return null;
54+
}
55+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
namespace ClawSharp.Infrastructure.Cron;
2+
3+
/// <summary>
4+
/// Represents a scheduled job with a name, schedule, and payload.
5+
/// </summary>
6+
public sealed class CronJob
7+
{
8+
/// <summary>Unique name of the job.</summary>
9+
public required string Name { get; set; }
10+
11+
/// <summary>The schedule that determines when the job fires.</summary>
12+
public required ISchedule Schedule { get; set; }
13+
14+
/// <summary>Optional payload data for the job (e.g., JSON, message text).</summary>
15+
public string? Payload { get; set; }
16+
17+
/// <summary>Optional session target for agent execution.</summary>
18+
public string? SessionTarget { get; set; }
19+
20+
/// <summary>When the job was last executed.</summary>
21+
public DateTimeOffset? LastRun { get; set; }
22+
23+
/// <summary>When the job was created.</summary>
24+
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
25+
26+
/// <summary>Whether the job is currently enabled.</summary>
27+
public bool Enabled { get; set; } = true;
28+
29+
/// <summary>Optional metadata for the job.</summary>
30+
public Dictionary<string, string>? Metadata { get; set; }
31+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using Cronos;
2+
3+
namespace ClawSharp.Infrastructure.Cron;
4+
5+
/// <summary>
6+
/// Schedule based on a standard cron expression (5 fields).
7+
/// Supports minute, hour, day of month, month, and day of week.
8+
/// </summary>
9+
public sealed class CronScheduleExpr : ISchedule
10+
{
11+
private readonly CronExpression _expression;
12+
private readonly string _expressionString;
13+
private readonly TimeZoneInfo _timeZone;
14+
15+
public ScheduleKind Kind => ScheduleKind.Cron;
16+
17+
/// <summary>The raw cron expression string.</summary>
18+
public string Expression => _expressionString;
19+
20+
/// <summary>
21+
/// Creates a cron schedule from the given expression.
22+
/// </summary>
23+
/// <param name="expression">Cron expression (5 fields: minute hour day-of-month month day-of-week).</param>
24+
/// <param name="timeZone">Optional timezone for evaluation. Defaults to UTC.</param>
25+
/// <exception cref="ArgumentException">Thrown if the expression is invalid.</exception>
26+
public CronScheduleExpr(string expression, TimeZoneInfo? timeZone = null)
27+
{
28+
ArgumentNullException.ThrowIfNull(expression);
29+
30+
try
31+
{
32+
_expression = CronExpression.Parse(expression);
33+
}
34+
catch (CronFormatException ex)
35+
{
36+
throw new ArgumentException($"Invalid cron expression: {expression}", nameof(expression), ex);
37+
}
38+
39+
_expressionString = expression;
40+
_timeZone = timeZone ?? TimeZoneInfo.Utc;
41+
}
42+
43+
public bool IsDue(DateTimeOffset now, DateTimeOffset? lastRun)
44+
{
45+
if (lastRun is null)
46+
return true;
47+
48+
// Get the next occurrence after the last run
49+
var nextOccurrence = GetNextOccurrence(lastRun.Value);
50+
51+
// It's due if the next occurrence is at or before now
52+
return nextOccurrence.HasValue && nextOccurrence.Value <= now;
53+
}
54+
55+
public DateTimeOffset? GetNextOccurrence(DateTimeOffset from)
56+
{
57+
var next = _expression.GetNextOccurrence(from.UtcDateTime, _timeZone);
58+
return next.HasValue ? new DateTimeOffset(next.Value, TimeSpan.Zero) : null;
59+
}
60+
}

0 commit comments

Comments
 (0)