Skip to content
Merged
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/pr_validation.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
name: pr_validation

on:
# Merge queues test a synthetic merge group rather than the pull request ref.
# Required checks must subscribe to this event or queued PRs will never merge.
merge_group:
types:
- checks_requested
push:
branches:
- master
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ name: smoke
# surface via GitHub's built-in Actions notifications.

on:
# Merge queues test a synthetic merge group rather than the pull request ref.
# Required checks must subscribe to this event or queued PRs will never merge.
merge_group:
types:
- checks_requested
workflow_dispatch:
schedule:
# 07:00 UTC daily — catches external-dependency drift during quiet
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<VersionPrefix>0.25.0</VersionPrefix>
<VersionSuffix>alpha.onnx.4</VersionSuffix>
<VersionSuffix>alpha.onnx.5</VersionSuffix>
<PackageReleaseNotes>Netclaw v0.25.0-beta.1 — SkillServer native sub-agent sync, memory curation unification, systemd PATH fix

**Features**
Expand Down
7 changes: 4 additions & 3 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
that trips NU1902 under our TreatWarningsAsErrors policy. -->
<AspireHostingVersion>13.4.6</AspireHostingVersion>
<CommunityToolkitAspireVersion>13.4.0</CommunityToolkitAspireVersion>
<ModelContextProtocolVersion>1.4.1</ModelContextProtocolVersion>
</PropertyGroup>
<!-- App dependencies -->
<ItemGroup>
Expand All @@ -43,8 +44,8 @@
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="$(MicrosoftExtensionsAIVersion)" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftAspNetCoreVersion)" />
<PackageVersion Include="Microsoft.Extensions.TimeProvider.Testing" Version="$(MicrosoftExtensionsAIVersion)" />
<PackageVersion Include="ModelContextProtocol.Core" Version="1.4.0" />
<PackageVersion Include="ModelContextProtocol.AspNetCore" Version="1.4.0" />
<PackageVersion Include="ModelContextProtocol.Core" Version="$(ModelContextProtocolVersion)" />
<PackageVersion Include="ModelContextProtocol.AspNetCore" Version="$(ModelContextProtocolVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="$(MicrosoftAspNetCoreVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.DataProtection" Version="$(MicrosoftAspNetCoreVersion)" />
<PackageVersion Include="System.Security.Cryptography.Xml" Version="$(MicrosoftAspNetCoreVersion)" />
Expand Down Expand Up @@ -96,7 +97,7 @@
trips NU1903 (GHSA-hv8m-jj95-wg3x, LZ4 decompression AccessViolation).
Keep this on the patched 3.x line already used by dev until Aspire ships
a non-vulnerable transitive dependency on its own. -->
<PackageVersion Include="MessagePack" Version="3.1.7" />
<PackageVersion Include="MessagePack" Version="3.1.8" />
</ItemGroup>
<!-- Image normalization (Netclaw.Media). SkiaSharp ships Windows/macOS native
assets in the base package; Linux needs the explicit headless native asset
Expand Down
23 changes: 23 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
# NetClaw Release Notes

## 0.25.0-alpha.onnx.5 (2026-07-12)

> **Experimental feature build** (fifth in the memory-embeddings series). This build carries
> **no changes to memory/embeddings behavior** versus `0.25.0-alpha.onnx.4` — it merges the
> mainline `dev` branch to bring the experimental line current with recent fixes, most
> importantly a fail-closed hardening of unattended sub-agent approvals. Same gating as before:
> everything rides `Memory.Embeddings.Enabled`, off by default; install only by exact pin
> (`NETCLAW_VERSION=0.25.0-alpha.onnx.5`). Upgrading from alpha.onnx.4 is a binary swap — no
> config, data, or unit changes.

### Security
- **Fail-closed unattended sub-agent approvals** — a sub-agent spawned from a session whose transport cannot service interactive approval prompts no longer inherits an approval bridge from the parent context; bridge presence alone can never make an unattended child interactive. Prevents an approval-gated tool from becoming silently auto-approvable in headless, webhook, and reminder-driven turns ([#1616](https://github.com/netclaw-dev/netclaw/pull/1616))

### Bug Fixes
- **Discord DM reminders** — reminders now fire correctly in Discord direct-message channels ([#1609](https://github.com/netclaw-dev/netclaw/pull/1609))
- **Slack processing-status updates serialized** — concurrent status updates on a Slack thread no longer race ([#1556](https://github.com/netclaw-dev/netclaw/pull/1556))
- **CLI model picker preserves hand-set modalities** — re-setting a model via `netclaw model set` or the picker no longer discards manually-configured input modalities ([#1610](https://github.com/netclaw-dev/netclaw/pull/1610))

### Dependency Updates
- ModelContextProtocol versioning consolidated into the central props file ([#1614](https://github.com/netclaw-dev/netclaw/pull/1614))
- MessagePack 3.1.7 → 3.1.8 ([#1605](https://github.com/netclaw-dev/netclaw/pull/1605))
- .NET SDK 10.0.300 → 10.0.301 ([#1381](https://github.com/netclaw-dev/netclaw/pull/1381))

## 0.25.0-alpha.onnx.4 (2026-07-09)

> **Experimental feature build** (fourth in the memory-embeddings series). Same gating:
Expand Down
15 changes: 13 additions & 2 deletions docs/integrations/discord-channel.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,20 @@ Discord channel target resolution accepts canonical channel forms:
- `<#123...>` (channel mention)
- `channel:123...` (explicit channel ID)

Discord direct-message target resolution accepts explicit user forms:

- `<@123...>` or `<@!123...>` (user mention)
- `dm:123...` or `@123...` (explicit user ID)

Bare Discord snowflakes are rejected because channel IDs and user IDs have the
same shape. Use `channel:<channelId>` for channel delivery or `dm:<userId>` for
direct-message delivery.

Reminder channel delivery maps to the generic `send_channel_message` tool with
`channel_key = "discord"` and a resolved destination object. Discord proactive
DM output is not supported yet.
`channel_key = "discord"` and a resolved destination object. Channel targets use
`destination.kind = "destination"`; user targets use
`destination.kind = "direct_message"` and are still gated by
`AllowDirectMessages` and `AllowedUserIds` at send time.

## Runtime behavior and troubleshooting

Expand Down
49 changes: 30 additions & 19 deletions docs/spec/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,24 +103,28 @@ keys used by model references.

### Models

Named model roles. Each role points to a provider and model ID.
Named model definitions own provider/model identity and metadata. Roles reference definitions,
so changing Main or Fallback does not destroy overrides belonging to the previous model.

```json
{
"Models": {
"Main": {
"Definitions": {
"qwen-main": {
"Provider": "remote-gpu",
"ModelId": "qwen3:30b",
"ContextWindow": 32768
},
"qwen-small": {
"Provider": "remote-gpu",
"ModelId": "qwen3:8b",
"ContextWindow": 32768
}
},
"Fallback": {
"Provider": "remote-gpu",
"ModelId": "qwen3:8b",
"ContextWindow": 32768
},
"Compaction": {
"Provider": "remote-gpu",
"ModelId": "qwen3:8b"
"Roles": {
"Main": "qwen-main",
"Fallback": "qwen-small",
"Compaction": "qwen-small"
}
}
}
Expand Down Expand Up @@ -481,8 +485,9 @@ following the standard .NET convention.

```bash
# Override the main model
export NETCLAW_Models__Main__Provider="openrouter"
export NETCLAW_Models__Main__ModelId="anthropic/claude-sonnet-4"
export NETCLAW_Models__Definitions__claude__Provider="openrouter"
export NETCLAW_Models__Definitions__claude__ModelId="anthropic/claude-sonnet-4"
export NETCLAW_Models__Roles__Main="claude"

# Set a provider API key
export NETCLAW_Providers__openrouter__ApiKey="sk-or-v1-..."
Expand Down Expand Up @@ -520,14 +525,20 @@ export NETCLAW_Session__MaxToolIterationsPerTurn="60"
}
},
"Models": {
"Main": {
"Provider": "local",
"ModelId": "qwen3:30b",
"ContextWindow": 32768
"Definitions": {
"qwen-main": {
"Provider": "local",
"ModelId": "qwen3:30b",
"ContextWindow": 32768
},
"qwen-small": {
"Provider": "local",
"ModelId": "qwen3:8b"
}
},
"Compaction": {
"Provider": "local",
"ModelId": "qwen3:8b"
"Roles": {
"Main": "qwen-main",
"Compaction": "qwen-small"
}
},
"Session": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ options instead of adding provider-specific properties to `ProviderEntry`.
### Degraded mode: No-Op chat client

When Netclaw starts without an explicitly configured main model/provider
(no `Models:Main`, incomplete `Models:Main`, no `Providers`, or `Models:Main`
(no `Models.Roles.Main`, an unresolved definition, no `Providers`, or the selected definition
points to a provider that is not configured), the daemon launches in
**degraded mode** with a No-Op chat client. Bound defaults such as
`local-ollama/qwen3:30b` do not count as operator configuration unless those
Expand Down Expand Up @@ -78,6 +78,35 @@ When adding an OpenAI provider from the CLI, `netclaw provider add <name>
openai` defaults to the ChatGPT OAuth device flow. Use `--auth api-key
--api-key <key>` to force platform API-key auth instead.

### Assigning models to roles and overriding metadata

`netclaw model set <role> <provider> <model-id>` creates or reuses a named model
definition and assigns it to a role (`main`, `fallback`, `compaction`). Definitions
own provider/model identity and metadata, while roles only reference definitions.
Switching away from a model and back therefore preserves its overrides. Two attributes can be overridden by the
operator and are **operator-owned**: the context window and the input/output
modalities. Provider discovery seeds a new definition but never changes an existing
definition, including adding a property the definition deliberately omits.

- `--context-window <tokens>` clamps the session budget and takes precedence
over provider-reported detection. Supplying it configures the model manually
and skips the metadata probe.
- `--input-modalities <list>` / `--output-modalities <list>` override detected
modalities with a comma-separated list of named flags (`Text`, `Image`,
`Audio`, `Video`). These do **not** skip the probe — the model is still
validated and its context window discovered; the override just wins over the
discovered modalities.
- `--clear-context-window` and `--clear-modalities` remove the respective
override so runtime capability detection resolves it again (use these after a
provider enlarges a model's window or fixes mis-reported modalities).

To change a preserved value you must pass the corresponding flag (a plain
re-set will not touch it). A legacy or hand-edited entry with an unreadable
value does not block a re-set — `model set` migrates legacy inline roles to named
definitions and repairs the selected entry while keeping the fields
it can still read; `model list` reports an unparseable config instead of
crashing, and `netclaw doctor --fix` repairs it.

### Adding GitHub Copilot

GitHub Copilot uses the OAuth device flow only — no API key. The operator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ Rules:
conversational follow-ups in Slack/TUI/SignalR sessions.
- `channel` requires both transport + address and resolves names/handles to
canonical IDs at set time; unresolved targets fail loud.
- Discord reminder targets must be explicit because channel IDs and user IDs are
both snowflakes: use `channel:<channelId>` or `<#channelId>` for channel posts,
and `dm:<userId>`, `@<userId>`, or `<@userId>` for DMs. Do not pass a bare
Discord ID.
- `none` runs silently (history still records execution).
- `expires_in` is not valid for `once` reminders; omit it for one-shot schedules.
- For recurring reminders that are permanently complete (PR merged, deploy done,
Expand Down
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "10.0.300",
"version": "10.0.301",
"rollForward": "major"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-10
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
## Context

Model metadata is currently embedded in three runtime role entries. Those entries are both the operator's durable configuration and the runtime consumer shape, so assigning a different model destroys metadata belonging to the previous model. Existing deployments and the current stable Docker image use this legacy shape.

## Goals / Non-Goals

**Goals:**

- Store model-owned metadata once in named definitions and make roles reference definitions.
- Keep manual JSON editing obvious: property absence means runtime detection, with no tombstones.
- Run legacy configuration without an eager write, and migrate deterministically on explicit mutation/fix.
- Resolve and validate references before persistence and runtime client construction.

**Non-Goals:**

- Downgrade compatibility after the configuration has been migrated.
- Automatic conflict resolution or model-definition garbage collection.
- Changes to provider discovery or actor/persistence protocols.

## Decisions

### New canonical shape

`Models.Definitions` is a dictionary of operator-chosen names to complete `ModelReference` values. `Models.Roles` contains `Main`, `Fallback`, and `Compaction` definition-name references. Runtime code receives the existing resolved `ModelSelection`, keeping actor and chat-client boundaries unchanged.

This is preferred over a hidden metadata cache or role-entry tombstones because it gives manual editors one visible source of truth and preserves property absence as runtime detection.

### Dual-shape reader, single-shape writer

A shared configuration resolver accepts either the complete legacy inline shape or the complete named shape. Mixed shapes, missing definitions, duplicate/invalid names, and conflicting migration candidates fail loudly. Daemon startup reads legacy configuration without rewriting it. CLI/TUI writes and `doctor --fix` migrate legacy input atomically before applying the requested mutation.

The schema accepts both complete shapes during the compatibility window. New writers emit only the named shape.

### Deterministic legacy migration

Each distinct case-insensitive `(Provider, ModelId)` becomes one definition. A deterministic slug is derived from provider and model ID, with a stable numeric suffix for name collisions. When multiple legacy roles identify the same model, their optional metadata must agree; otherwise migration fails with the conflicting role names and fields.

### Upgrade smoke

The smoke harness creates a disposable directory/volume, runs the latest stable image to produce or consume a legacy configuration, stops it, builds a uniquely tagged local image, and starts that image against the same isolated volume. Assertions verify startup, legacy resolution, explicit migration, and preservation after role switching. Cleanup removes only resources carrying the test's unique label/name.

## Risks / Trade-offs

- **Older binaries cannot read the named shape after migration** → document that rollback requires restoring the pre-migration backup; migration writes atomically and retains a backup.
- **Dual-shape support can become permanent complexity** → centralize it in one resolver and have every writer emit only the canonical shape.
- **Conflicting legacy roles could be silently merged** → reject conflicts and report exact fields/roles.
- **Docker smoke could touch operator state** → require an absolute temporary path created by the harness and unique container/image names; never use default Netclaw volumes.
- **Stable image availability/network failures** → make the Docker upgrade scenario explicit and fail with actionable diagnostics; unit migration fixtures remain mandatory offline proof.

## Migration Plan

1. Ship a reader that supports both shapes and schema validation for both.
2. Verify an untouched legacy stable configuration starts with the new daemon.
3. On the first explicit model/config write or `doctor --fix`, validate, back up, migrate, re-resolve, then atomically persist.
4. Document rollback as restoring the generated legacy backup before running an older image.

## Open Questions

- The exact stable tag is resolved from the release manifest at smoke execution time rather than hard-coded.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
## Why

PRD-004 and PRD-005 allow operators to select models and override provider-reported capabilities, but the current `Models.Main` / `Fallback` / `Compaction` entries combine role assignment with model-owned metadata. Switching a role therefore destroys manually maintained context-window and modality overrides, especially for vLLM deployments that cannot report modalities.

## What Changes

- Add human-readable named model definitions whose metadata is independent of role assignment.
- Make model roles reference named definitions, so switching roles does not rewrite a definition.
- Continue accepting the existing inline role shape on upgrade and provide deterministic migration to the named shape.
- Reject ambiguous mixed or conflicting configuration instead of silently choosing a representation.
- Add isolated stable-container to locally-built-container upgrade smoke coverage using a disposable volume.
- Preserve absence of optional metadata as runtime detection; no hidden tombstone values are introduced.

In scope: configuration binding, CLI/TUI model assignment, schema, doctor/migration behavior, operational guidance, automated compatibility proof, and Docker upgrade smoke coverage.

Out of scope: automatic model discovery beyond existing probes, changing provider APIs, and supporting downgrade from the new shape to an older Netclaw binary.

## Capabilities

### New Capabilities

- `named-model-definitions`: Model-owned definitions, role references, legacy resolution, migration, and conflict behavior.

### Modified Capabilities

- `netclaw-model-providers`: Primary, fallback, and compaction assignments reference persistent model definitions.
- `netclaw-cli`: Model commands and TUI preserve model metadata across role switches and expose migration failures.
- `netclaw-testing`: Upgrade compatibility is proven with legacy configuration and an isolated container-volume smoke.

## Impact

Affected areas include `Netclaw.Configuration` model types and schema, daemon/CLI configuration binding, model CLI and TUI persistence, provider rename behavior, doctor repair, system operational guidance, and smoke tooling. Startup remains fail-closed for invalid references. Existing inline deployments remain readable and runnable without an eager startup rewrite.

Security impact is limited to configuration integrity: unresolved or conflicting role references fail before persistence or runtime client construction. Operationally, configuration is migrated only by an explicit writing/fix operation, and the upgrade smoke never mounts the operator's real Netclaw home.
Loading
Loading