Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Empty file added .ai/mcp/mcp.json
Empty file.
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,12 @@ dist/
.reviews/
.review/

# Agent memory
# AI agent artifacts
.ai/
.claude/agent-memory/

# Private keys — never commit signing keys or credentials
*.key

# Svelte/Node frontend
**/node_modules/
11 changes: 5 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,11 @@ Business logic uses vertical slice architecture with `Immediate.Handlers` (sourc

```
Features/
Chat/ (4 handlers — SendMessage, BuildChatRequest, ProcessToolCalls, CompactSession)
Session/ (5 handlers — Load, Save, Clear, List, Prune)
Cost/ (3 handlers — CheckBudget, RecordUsage, GetSummary)
Memory/ (5 handlers — Store, Search, Recall, ExtractFacts, Decay)
Tools/ (1 handler — ExecuteTool)
Behaviors/ (pipeline behaviors — validation, logging)
Chat/ (4 handlers — ApplySecurityGuards, SanitizeReply, BuildChatRequest, RouteModel)
Session/ (4 handlers — Load, Save, Clear, Prune)
Cost/ (3 handlers — CheckBudget, RecordUsage, GetCostSummary)
Memory/ (5 handlers — WriteMemory, ClearMemory, ExtractFacts, SearchMemory, GetMemoryContext)
Behaviors/ (pipeline behaviors — authorization, logging)
```

Generated registration methods: `AddclawsharpHandlers()` / `AddclawsharpBehaviors()` (lowercase 'c' — uses raw assembly name). Handler lifetime is `ServiceLifetime.Singleton`.
Expand Down
19 changes: 14 additions & 5 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,16 @@ services:
tmpfs:
- /tmp # .NET runtime temp files
- /var/tmp # some system libs expect this
network_mode: host
ports:
- "127.0.0.1:3001:3001"
extra_hosts:
- "host.docker.internal:host-gateway"
healthcheck:
test: ["CMD-SHELL", "dotnet /app/clawsharp.dll doctor 2>/dev/null || exit 1"]
interval: 30s
timeout: 5s
start_period: 15s
retries: 3
volumes:
# Persists config.json, sessions, memory, skills, and the .secret_key file.
# The .secret_key file is only used when neither CLAWSHARP_SECRET_KEY env var
Expand All @@ -80,18 +89,18 @@ services:
CLAWSHARP__channels__web__enabled: "true"
CLAWSHARP__channels__web__webHost: "0.0.0.0"
CLAWSHARP__channels__web__webPort: "3001"
CLAWSHARP__channels__web__pairingToken: "test-token-123"
CLAWSHARP__channels__web__pairingToken: "${CLAWSHARP_WEB_PAIRING_TOKEN:?Set CLAWSHARP_WEB_PAIRING_TOKEN in .env}"

# ── PostgreSQL memory backend ──────────────────────────────────────────
CLAWSHARP__memory__backend: postgres
CLAWSHARP__memory__connectionString: "Host=127.0.0.1;Database=clawsharp;Username=clawsharp;Password=${POSTGRES_PASSWORD}"
CLAWSHARP__memory__connectionString: "Host=postgres;Database=clawsharp;Username=clawsharp;Password=${POSTGRES_PASSWORD}"

# ── Analytics (interactions) → PostgreSQL ──────────────────────────────
CLAWSHARP__analytics__enabled: "true"
CLAWSHARP__analytics__backend: postgres

# ── LM Studio (host network — use localhost) ───────────────────────────
CLAWSHARP__providers__lmstudio__baseUrl: "http://127.0.0.1:1234"
# ── LM Studio (via host.docker.internal) ────────────────────────────────
CLAWSHARP__providers__lmstudio__baseUrl: "http://host.docker.internal:1234"
CLAWSHARP__agents__defaults__model: "qwen/qwen3.5-9b"

# ── Optional: inline config overrides (no file needed) ──────────────────
Expand Down
7 changes: 7 additions & 0 deletions nuget.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>
40 changes: 22 additions & 18 deletions src/clawsharp-sign/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,15 @@
// Derive package name from directory name or primary plugin DLL
var package = DerivePackageName(pluginDir, dllFiles);

// Build manifest without signature for signing
var timestamp = DateTimeOffset.UtcNow.ToString("O");

// Build manifest without signature for signing (canonical payload — no timestamp)
var manifestData = new ManifestData
{
Files = new SortedDictionary<string, string>(files, StringComparer.Ordinal),
KeyId = keyId,
Package = package,
Version = version,
KeyId = keyId,
Timestamp = DateTimeOffset.UtcNow.ToString("O"),
Files = new SortedDictionary<string, string>(files, StringComparer.Ordinal),
};

// Canonical JSON: sorted keys, no whitespace
Expand All @@ -141,7 +142,7 @@
Package = manifestData.Package,
Version = manifestData.Version,
KeyId = manifestData.KeyId,
Timestamp = manifestData.Timestamp,
Timestamp = timestamp,
Files = manifestData.Files,
Signature = signatureBase64,
};
Expand Down Expand Up @@ -194,13 +195,13 @@
}

// Step 1: Verify signature over canonical manifest payload (D-30: signature first)
// Canonical payload excludes timestamp — must match signer's ManifestData shape
var manifestData = new ManifestData
{
Files = signedManifest.Files,
KeyId = signedManifest.KeyId,
Package = signedManifest.Package,
Version = signedManifest.Version,
KeyId = signedManifest.KeyId,
Timestamp = signedManifest.Timestamp,
Files = signedManifest.Files,
};

var canonicalBytes = JsonSerializer.SerializeToUtf8Bytes(manifestData, ManifestJsonContext.Default.ManifestData);
Expand Down Expand Up @@ -266,7 +267,7 @@
{
for (var i = 0; i < args.Length - 1; i++)
{
if (string.Equals(args[i], flag, StringComparison.OrdinalIgnoreCase))

Check warning on line 270 in src/clawsharp-sign/Program.cs

View workflow job for this annotation

GitHub Actions / Qodana Community for .NET

Use preferred braces style: Enforce braces in 'if' statement

Inconsistent braces style: missing braces
return args[i + 1];
}

Expand All @@ -279,7 +280,7 @@
var pluginDll = dllFiles.FirstOrDefault(f =>
f.StartsWith("clawsharp.Plugin.", StringComparison.OrdinalIgnoreCase) && f.EndsWith(".dll", StringComparison.OrdinalIgnoreCase));

if (pluginDll is not null)

Check warning on line 283 in src/clawsharp-sign/Program.cs

View workflow job for this annotation

GitHub Actions / Qodana Community for .NET

Use preferred braces style: Enforce braces in 'if' statement

Inconsistent braces style: missing braces
return Path.GetFileNameWithoutExtension(pluginDll);

// Fall back to directory name
Expand Down Expand Up @@ -307,23 +308,26 @@

// ── JSON DTOs ───────────────────────────────────────────────────────────

/// <summary>Manifest data without signature — the canonical payload that gets signed.</summary>
/// <summary>
/// Manifest data without signature — the canonical payload that gets signed.
/// Properties are in alphabetical order by JSON key to match the verifier's
/// <c>SortedDictionary</c>-based canonical payload (STJ source-gen serializes
/// in declaration order). Timestamp is excluded from the signed payload —
/// it is metadata in the full <see cref="SignedManifest"/> only.
/// </summary>
internal sealed class ManifestData
{
[JsonPropertyName("package")]
public string Package { get; init; } = "";

[JsonPropertyName("version")]
public string Version { get; init; } = "";
[JsonPropertyName("files")]
public SortedDictionary<string, string> Files { get; init; } = new(StringComparer.Ordinal);

[JsonPropertyName("keyId")]
public string KeyId { get; init; } = "";

[JsonPropertyName("timestamp")]
public string Timestamp { get; init; } = "";
[JsonPropertyName("package")]
public string Package { get; init; } = "";

[JsonPropertyName("files")]
public SortedDictionary<string, string> Files { get; init; } = new(StringComparer.Ordinal);
[JsonPropertyName("version")]
public string Version { get; init; } = "";
}

/// <summary>Full manifest with signature — written to plugin.manifest.json.</summary>
Expand Down
16 changes: 14 additions & 2 deletions src/clawsharp-web/src/lib/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,29 @@ function escapeHtml(s: string): string {
.replace(/>/g, '&gt;');
}

function isSafeUrl(url: string): boolean {
try {
const parsed = new URL(url, window.location.origin);
return ['http:', 'https:', 'mailto:'].includes(parsed.protocol);
} catch {
return false;
}
}

function inlineMarkdown(s: string): string {
// Inline code — must come before bold/italic
s = s.replace(/`([^`]+)`/g, '<code>$1</code>');
// Bold
s = s.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
// Italic
s = s.replace(/\*(.+?)\*/g, '<em>$1</em>');
// Links
// Links — only allow safe protocols (http, https, mailto)
s = s.replace(
/\[([^\]]+)\]\(([^)]+)\)/g,
'<a href="$2" target="_blank" rel="noopener">$1</a>',
(_, text, url) =>
isSafeUrl(url)
? `<a href="${url}" target="_blank" rel="noopener">${text}</a>`
: text,
);
return s;
}
Expand Down
2 changes: 1 addition & 1 deletion src/clawsharp.Plugin.Confluence/ConfluenceApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
/// <summary>
/// HTTP client for the Confluence REST API v2 with cursor-based pagination per D-10.
/// Uses a named <see cref="HttpClient"/> injected via DI with SsrfGuard-protected
/// <see cref="System.Net.Sockets.SocketsHttpHandler.ConnectCallback"/> per D-26.
/// <see cref="SocketsHttpHandler.ConnectCallback"/> per D-26.
/// </summary>
internal sealed class ConfluenceApiClient
{
private readonly HttpClient _httpClient;

public ConfluenceApiClient(HttpClient httpClient)

Check notice on line 16 in src/clawsharp.Plugin.Confluence/ConfluenceApiClient.cs

View workflow job for this annotation

GitHub Actions / Qodana Community for .NET

Convert constructor into primary constructor

Convert into primary constructor
{
_httpClient = httpClient;
}
Expand Down
2 changes: 1 addition & 1 deletion src/clawsharp.Plugin.Gcs/GcsPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
StorageClient storageClient;
if (config.CredentialPath is not null)
{
var credential = GoogleCredential.FromFile(config.CredentialPath);

Check warning on line 32 in src/clawsharp.Plugin.Gcs/GcsPlugin.cs

View workflow job for this annotation

GitHub Actions / Qodana Community for .NET

Use of obsolete symbol

CS0618: Method 'Google.Apis.Auth.OAuth2.GoogleCredential.FromFile(string)' is obsolete: 'This method is being deprecated because of a potential security risk. Use the methods in the CredentialFactory class instead. A GoogleCredential object can then be created by calling the .ToGoogleCredential() method on the returned specific credential. '
storageClient = StorageClient.Create(credential);
}
else
Expand Down Expand Up @@ -65,9 +65,9 @@
/// </summary>
private static bool IsPrivateIpLikeName(string name)
{
if (!System.Net.IPAddress.TryParse(name, out var ip))

Check warning on line 68 in src/clawsharp.Plugin.Gcs/GcsPlugin.cs

View workflow job for this annotation

GitHub Actions / Qodana Community for .NET

Use preferred braces style: Enforce braces in 'if' statement

Inconsistent braces style: missing braces
return false;

return Clawsharp.Security.SsrfGuard.IsPrivateOrReservedAddress(ip);
return Security.SsrfGuard.IsPrivateOrReservedAddress(ip);
}
}
4 changes: 2 additions & 2 deletions src/clawsharp/A2a/A2aAgentCardBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
/// <summary>
/// Builds the Agent Card for A2A discovery (/.well-known/agent-card.json).
/// Skills are derived 1:1 from the tool registry, filtered to Low/Medium sensitivity (D-10).
/// Capabilities reflect runtime config (D-12). Metadata follows config override chains (D-13).
/// Card is built once at startup and cached — tool registry is immutable at runtime (D-11).
/// Capabilities reflect runtime config (D-12). Name falls back to "ClawSharp Agent" when
/// <c>a2a.agentCard.name</c> is null. Card is built once at startup and cached (D-11).
/// </summary>
public sealed class A2aAgentCardBuilder(
IToolRegistry toolRegistry,
Expand All @@ -28,7 +28,7 @@
var sensitivity = toolRegistry.GetToolSensitivity(def.Name);

// D-10: Filter to Low/Medium sensitivity only. Skip High and Critical.
if (sensitivity > ToolSensitivity.Medium)

Check warning on line 31 in src/clawsharp/A2a/A2aAgentCardBuilder.cs

View workflow job for this annotation

GitHub Actions / Qodana Community for .NET

Use preferred braces style: Enforce braces in 'if' statement

Inconsistent braces style: missing braces
continue;

skills.Add(new AgentSkill
Expand Down Expand Up @@ -86,7 +86,7 @@
// D-16: Security schemes declaration
SecuritySchemes = new Dictionary<string, SecurityScheme>
{
["bearer"] = new SecurityScheme

Check notice on line 89 in src/clawsharp/A2a/A2aAgentCardBuilder.cs

View workflow job for this annotation

GitHub Actions / Qodana Community for .NET

Use preferred style of 'new' expression when created type is evident

Redundant type specification
{
HttpAuthSecurityScheme = new HttpAuthSecurityScheme
{
Expand Down
14 changes: 14 additions & 0 deletions src/clawsharp/A2a/A2aAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,18 @@ internal static class A2aAttributes

/// <summary>Unique chain identifier correlating delegation hops across instances.</summary>
internal const string DelegationChainId = "a2a.delegation.chain_id";

// ── Cooperative delegation metadata keys (propagated in A2A task metadata) ──

/// <summary>Metadata key: current delegation depth (incremented per hop).</summary>
internal const string MetaDepth = "clawsharp.delegation.depth";

/// <summary>Metadata key: maximum allowed delegation depth.</summary>
internal const string MetaMaxDepth = "clawsharp.delegation.maxDepth";

/// <summary>Metadata key: machine name of the originating instance.</summary>
internal const string MetaOriginInstance = "clawsharp.delegation.originInstance";

/// <summary>Metadata key: unique chain identifier for correlating delegation hops.</summary>
internal const string MetaChainId = "clawsharp.delegation.chainId";
}
6 changes: 3 additions & 3 deletions src/clawsharp/A2a/A2aClientConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace Clawsharp.A2a;
/// Client-side A2A delegation config. Added to <see cref="A2aConfig"/> as nullable Client property.
/// Null = no delegation capability (zero tools registered).
/// </summary>
public sealed record A2aClientConfig
public sealed class A2aClientConfig
{
/// <summary>Max delegation chain depth. Default: 3.</summary>
/// <remarks>Uses set (not init) so STJ source-gen preserves defaults on deserialization.</remarks>
Expand All @@ -19,7 +19,7 @@ public sealed record A2aClientConfig
}

/// <summary>Configuration for a single trusted external A2A agent.</summary>
public sealed record TrustedAgentConfig
public sealed class TrustedAgentConfig
{
/// <summary>Base URL of the external agent's A2A endpoint.</summary>
public required string Url { get; init; }
Expand All @@ -32,7 +32,7 @@ public sealed record TrustedAgentConfig
}

/// <summary>Authentication credentials for a trusted agent. Supports bearer token and API key.</summary>
public sealed record AgentAuthConfig
public sealed class AgentAuthConfig
{
/// <summary>Auth type: "bearer" or "apiKey".</summary>
public required string Type { get; init; }
Expand Down
Loading
Loading