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
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ Working branch: `refactor/binding-engine`, stacked on `refactor/delivery-failure

- [x] 5.1 Full solution build with zero warnings; full `Netclaw.Actors.Tests`, `Netclaw.Daemon.Tests` channel suites; slopwatch and header gates
- [x] 5.2 Line-count accounting for the PR description (target ~1,200-1,500 lines removed) and parity-spec sync via `/opsx-sync`
- [ ] 5.3 Submit `refactor/binding-engine` as PR 4 via `gh stack submit`; PR body documents any differences surfaced by the stop rule
- [x] 5.3 Submit `refactor/binding-engine` as PR 4 via `gh stack submit`; PR body documents any differences surfaced by the stop rule
24 changes: 14 additions & 10 deletions openspec/specs/audience-context-filtering/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,7 @@ fallback.
## Requirements
### Requirement: Context layer audience filtering

The context layer system SHALL accept a `TrustAudience` parameter on
`IContextLayerProvider.GetContextLayer()`. Each context layer implementation
SHALL use the audience to determine what content to return. The
`ContextAssemblyInput` record SHALL include a `TrustAudience Audience` field.
When a feature is disabled deployment-wide, the corresponding context layer
SHALL also return empty even for non-Public audiences.
The context layer system SHALL accept a `TrustAudience` parameter on `IContextLayerProvider.GetContextLayer()`. Each context layer implementation SHALL use the audience to determine what content to return. The `ContextAssemblyInput` record SHALL include a `TrustAudience Audience` field. When a feature is disabled deployment-wide, the corresponding context layer SHALL also return empty even for non-Public audiences. The skill context layer SHALL use separate Team and Personal index values when source permissions differ.

#### Scenario: Public audience receives no skill index

Expand All @@ -47,15 +42,23 @@ SHALL also return empty even for non-Public audiences.
- **THEN** `SkillIndexContextLayer.GetContextLayer(Team)` returns empty string
- **AND** no skill index appears in the session's system messages

#### Scenario: Team audience receives all allowed context layers
#### Scenario: Team audience receives allowed context layers

- **WHEN** a Team-audience session assembles context
- **THEN** all enabled context layers return their full content
- **THEN** all enabled context layers return their allowed content

#### Scenario: Personal audience receives all allowed context layers
#### Scenario: Personal audience receives allowed context layers

- **WHEN** a Personal-audience session assembles context
- **THEN** all enabled context layers return their full content
- **THEN** all enabled context layers return their allowed content

#### Scenario: MCP prompt server differs by audience

- **GIVEN** Personal can use MCP server `gigatron`
- **AND** Team cannot use MCP server `gigatron`
- **WHEN** both audiences request the skill context layer
- **THEN** the Personal index contains `mcp__gigatron__` prompt skills
- **AND** the Team index does not reveal those skill names

### Requirement: Session block path redaction

Expand Down Expand Up @@ -150,3 +153,4 @@ fail loudly rather than adopt a default audience.
- **WHEN** the pipeline derives the effective audience
- **THEN** the derived audience reflects the Personal source audience
- **AND** no default-audience value participates in the derivation

117 changes: 117 additions & 0 deletions openspec/specs/daemon-shell-path/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# daemon-shell-path Specification

## Purpose

Define how the installed Netclaw daemon gets the operator's real `PATH`. A
systemd `--user` service does not inherit the login shell environment. Netclaw
therefore captures the operator `PATH` at install time and delivers it through a
netclaw-owned environment file. Install, `doctor`, and uninstall keep that file
correct.

## Requirements

### Requirement: Installed service PATH is captured from the operator environment, not guessed

When installing the systemd `--user` service, `netclaw daemon install` SHALL provision the
daemon's `PATH` by capturing the operator's real `PATH` from the CLI process's own inherited
environment. It SHALL NOT write a hardcoded/guessed list of directories, and it SHALL NOT execute
a shell or source operator dotfiles to obtain the value. The captured `PATH` SHALL be delivered to
the daemon via a netclaw-owned environment file referenced by the unit's `EnvironmentFile=`
directive, and the generated unit SHALL NOT contain an inline `Environment=PATH=` directive.

The provisioned `PATH` value SHALL place the daemon's own install directory first, then the
captured operator `PATH`, then a guaranteed system-directory floor
(`/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`) so the daemon's shell remains functional even when
the installing shell's `PATH` was empty or partial. Entries SHALL be de-duplicated, and empty
`PATH` elements (which POSIX resolves to the current directory) SHALL be dropped.

#### Scenario: Install captures the caller's PATH into the environment file

- **WHEN** the operator runs `netclaw daemon install` from a shell whose `PATH` includes
`~/.dotnet`
- **THEN** the netclaw-owned environment file contains a `PATH=` line that includes `~/.dotnet`
- **AND** the file's `PATH` begins with the daemon's install directory
- **AND** no shell process was spawned to read the `PATH`

#### Scenario: Generated unit wires the environment file and omits inline PATH

- **WHEN** `netclaw daemon install` writes `~/.config/systemd/user/netclaw.service`
- **THEN** the unit contains an `EnvironmentFile=` directive pointing at the netclaw-owned
environment file
- **AND** the unit does NOT contain an `Environment=PATH=` directive

### Requirement: `doctor --fix` rehydrates the daemon PATH environment file

When the installed unit references the daemon PATH environment file, `netclaw doctor --fix` SHALL
rehydrate that file from the current shell's `PATH` when it is missing or does not include the
daemon's install directory. Rehydration SHALL run independently of whether the application config
file (`netclaw.json`) exists. The fix SHALL write files only — creating the parent directory if it
is absent — and SHALL surface an explicit instruction to run `systemctl --user restart netclaw`; it
SHALL NOT restart the daemon implicitly. `doctor --fix` SHALL NOT rewrite the systemd unit: a unit
that does not reference the environment file (a legacy inline-`PATH` unit) is a reinstall case,
routed by the doctor check.

#### Scenario: Missing environment file is recreated by the fix

- **WHEN** the systemd unit is installed but the daemon PATH environment file is absent
- **AND** the operator runs `netclaw doctor --fix`
- **THEN** the fix writes the environment file from the current shell's `PATH`
- **AND** the fix output instructs the operator to `systemctl --user restart netclaw`
- **AND** the fix does not restart the daemon

#### Scenario: Rehydration runs even when the app config file is absent

- **WHEN** `netclaw.json` does not exist
- **AND** the systemd unit is installed but its PATH environment file is stale or missing
- **AND** the operator runs `netclaw doctor --fix`
- **THEN** the environment-file rehydration fix is still evaluated and applied

### Requirement: Systemd PATH doctor check validates the environment-file wiring

`SystemdUnitPathDoctorCheck` SHALL validate that the installed unit references the daemon PATH
environment file via `EnvironmentFile=`, that the referenced file exists, and that the file's
`PATH` includes the daemon's install directory. When the referenced file is missing or omits the
install directory, remediation SHALL point the operator at `netclaw doctor --fix` followed by a
restart. A legacy unit that supplies its `PATH` inline (`Environment=PATH=`) SHALL pass when that
inline `PATH` includes the install directory (with a note to migrate via reinstall), and SHALL warn
with a reinstall remediation otherwise. The check SHALL pass silently when no service is installed
or on non-Linux platforms.

#### Scenario: Wired, present, and install-dir on PATH passes

- **WHEN** the unit references the environment file, the file exists, and its `PATH` includes the
install directory
- **THEN** the check passes

#### Scenario: Functional legacy inline-PATH unit passes with a migration note

- **WHEN** the installed unit has no `EnvironmentFile=` but supplies an inline `Environment=PATH=`
that includes the install directory
- **THEN** the check passes and notes that re-running `netclaw daemon install` migrates it

#### Scenario: Unwired or broken legacy unit warns with reinstall remediation

- **WHEN** the installed unit has no `EnvironmentFile=` and no inline `PATH` that includes the
install directory
- **THEN** the check returns a warning with remediation to reinstall and restart

#### Scenario: Referenced environment file absent warns

- **WHEN** the unit references the environment file but the file does not exist on disk
- **THEN** the check returns a warning with remediation to run `netclaw doctor --fix` and restart

#### Scenario: No service installed skips

- **WHEN** no netclaw systemd unit file exists
- **THEN** the check passes without warning

### Requirement: Uninstall removes the daemon PATH environment file

`netclaw daemon uninstall` SHALL remove the netclaw-owned daemon PATH environment file in addition
to the unit file, leaving no orphaned environment file behind.

#### Scenario: Uninstall deletes the environment file

- **WHEN** the operator runs `netclaw daemon uninstall` with an installed service and an existing
daemon PATH environment file
- **THEN** both the unit file and the daemon PATH environment file are deleted
60 changes: 60 additions & 0 deletions openspec/specs/named-model-definitions/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# named-model-definitions Specification

## Purpose

Define named model definitions that own provider identity, model ID, and
operator capability overrides. Model roles reference a definition by name. A
role switch therefore does not destroy the metadata of the definition it leaves.
This capability also defines legacy configuration acceptance, migration, and
conflict behavior.

## 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
Loading
Loading