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
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
2 changes: 1 addition & 1 deletion feeds/skills/.system/files/netclaw-operations/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: netclaw-operations
description: "REQUIRED when the user asks about scheduling, reminders, cron jobs, timers, background jobs, diagnostics, troubleshooting, MCP tools, daemon health, identity updates, or Netclaw capabilities and self-maintenance."
metadata:
author: netclaw
version: "2.24.1"
version: "2.26.0"
---

# Netclaw Operations
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
@@ -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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
## ADDED Requirements

### Requirement: Model-owned named definitions
The system SHALL store provider identity, model ID, context-window override, modality overrides, and provenance in named model definitions independent of runtime role assignment. Roles SHALL reference definitions by name.

#### Scenario: Switching away and back preserves overrides
- **GIVEN** definition `vision` has a manual `InputModalities` override
- **WHEN** Main switches from `vision` to another definition and back
- **THEN** the `vision` definition SHALL remain unchanged
- **AND** Main SHALL resolve to its original override

#### Scenario: Manual absence remains runtime detection
- **GIVEN** an existing definition omits an optional capability property
- **WHEN** the definition is assigned to another role
- **THEN** the property SHALL remain absent
- **AND** no tombstone or discovered replacement SHALL be persisted

### Requirement: Legacy model configuration compatibility
The system SHALL accept the legacy inline Main/Fallback/Compaction shape without rewriting it during startup and SHALL resolve it to the same runtime model selection.

#### Scenario: Existing deployment starts after upgrade
- **GIVEN** a valid configuration written by the latest stable Netclaw image
- **WHEN** the upgraded daemon starts
- **THEN** startup SHALL succeed with equivalent model role values and capabilities
- **AND** the configuration file SHALL not be rewritten merely by startup

#### Scenario: Explicit mutation migrates legacy shape
- **GIVEN** a valid legacy configuration
- **WHEN** an operator performs a model-writing command or runs doctor fix
- **THEN** the system SHALL atomically persist the named shape before completing the mutation
- **AND** the persisted named shape SHALL resolve to the same runtime values

#### Scenario: Ambiguous shape fails loudly
- **GIVEN** configuration contains both legacy role objects and named role references
- **WHEN** configuration is validated or loaded
- **THEN** the operation SHALL fail with remediation identifying the mixed shape

### Requirement: Reference integrity
Every persisted role reference SHALL resolve to an existing definition before persistence and startup.

#### Scenario: Missing definition is rejected
- **WHEN** a role references an unknown definition
- **THEN** validation SHALL fail before runtime client construction
- **AND** no partial configuration write SHALL occur

#### Scenario: Conflicting legacy duplicates are rejected
- **GIVEN** two legacy roles identify the same provider/model but contain conflicting overrides
- **WHEN** migration is requested
- **THEN** migration SHALL fail with the conflicting roles and fields
- **AND** the legacy file SHALL remain unchanged
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
## ADDED Requirements

### Requirement: Named model role management
Model CLI and TUI operations SHALL assign roles by changing references and SHALL edit model metadata only through the selected definition.

#### Scenario: Assign existing definition
- **WHEN** an operator assigns an existing named definition to Main
- **THEN** only the Main role reference SHALL change
- **AND** no definition metadata SHALL change

#### Scenario: Mutating legacy configuration
- **GIVEN** the CLI loads a valid legacy model configuration
- **WHEN** a model mutation is requested
- **THEN** the CLI SHALL migrate and validate the canonical shape before persistence
- **AND** failure SHALL leave the original file unchanged
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## MODIFIED Requirements

### Requirement: Primary and fallback model
The system SHALL support configuring primary, fallback, and compaction roles as references to persistent named model definitions. Changing a role SHALL NOT change the referenced definition. When the primary model is unavailable due to rate limiting, timeout, or error, the system SHALL automatically switch to the fallback model. Fallback activation SHALL be logged for operator visibility.

#### Scenario: Primary model succeeds
- **GIVEN** both primary and fallback roles reference valid definitions
- **WHEN** the primary model responds successfully
- **THEN** the primary model response SHALL be used
- **AND** no fallback activation SHALL occur

#### Scenario: Automatic fallback on primary failure
- **GIVEN** both primary and fallback roles reference valid definitions
- **WHEN** the primary model returns a rate limit, timeout, or error response
- **THEN** the system SHALL retry using the fallback definition
- **AND** a log entry SHALL record the fallback activation with the failure reason

#### Scenario: Role switch preserves model definition
- **GIVEN** a named model definition contains operator capability overrides
- **WHEN** Main or Fallback is assigned to another definition
- **THEN** the previous definition SHALL remain unchanged and available for reassignment
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
## ADDED Requirements

### Requirement: Container upgrade compatibility proof
The smoke suite SHALL verify upgrade from the latest stable Netclaw container to a locally built image using only an isolated temporary configuration volume.

#### Scenario: Stable-to-local upgrade
- **GIVEN** the latest stable image has written or consumed a legacy config in a disposable volume
- **WHEN** a uniquely tagged local image starts against the same volume
- **THEN** the new image SHALL become healthy without modifying the file on startup
- **AND** an explicit migration SHALL preserve effective role and capability values
- **AND** switching away from and back to a definition SHALL preserve its overrides

#### Scenario: Production state isolation
- **WHEN** the upgrade smoke runs
- **THEN** it SHALL use a newly created absolute temporary directory or uniquely named test volume
- **AND** it SHALL NOT mount or inspect the default or operator-provided Netclaw home
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
## 1. Canonical configuration and compatibility

- [x] 1.1 Add named definition and role-reference configuration types plus one resolver for legacy and canonical shapes
- [x] 1.2 Add deterministic, conflict-detecting legacy migration with atomic persistence and backup behavior
- [x] 1.3 Update the JSON schema to accept legacy or canonical models while rejecting mixed/invalid shapes
- [x] 1.4 Route daemon, CLI, doctor, provider rename, wizard, and TUI consumers through resolved canonical configuration

## 2. Operator workflows

- [x] 2.1 Update model CLI commands to create/edit definitions and switch roles without mutating definitions
- [x] 2.2 Update the model-manager TUI and initialization writer to emit canonical configuration
- [x] 2.3 Update `netclaw-operations` guidance and CLI help, including migration and rollback behavior

## 3. Automated proof

- [x] 3.1 Add legacy load, conflict rejection, canonical round-trip, and role A→B→A preservation tests
- [x] 3.2 Add CLI/TUI tests proving invalid references are rejected before persistence
- [x] 3.3 Add isolated latest-stable-container → local-image upgrade smoke and semantic assertions
- [ ] 3.4 Run targeted/full tests, native TUI smoke, evals, Slopwatch, and copyright verification
- [x] 3.5 Validate OpenSpec implementation alignment and prepare spec synchronization
Loading
Loading