Summary
netclaw model set --input-modalities / --output-modalities accept raw integers for single-flag values, contrary to the intended contract stated in the code's own comment.
Repro
Verified against netclaw 0.25.0-alpha.onnx.7 (commit 5c7278f) in an isolated NETCLAW_HOME:
$ netclaw model set main local qwen3:30b --input-modalities 1
Set main model to local/qwen3:30b # exit 0
Resulting definition — the integer is silently coerced to a named flag:
"local-qwen3-30b": {
"Provider": "local",
"ModelId": "qwen3:30b",
"Provenance": "Manual",
"InputModalities": "Text"
}
For contrast, these are correctly rejected:
$ netclaw model set main local qwen3:30b --input-modalities 3
Error: invalid modalities '3'. Use a comma-separated list of: Text, Image, Audio, Video (...) # exit 1
$ netclaw model set main local qwen3:30b --input-modalities Bogus
Error: invalid modalities 'Bogus'. ... # exit 1
| Input |
Actual |
Expected |
1, 2, 4, 8 |
accepted → Text/Image/Audio/Video |
rejected |
3 (composite), 0, 99, Bogus |
rejected |
rejected |
Cause
ModelCommand.cs:313-315 guards with !Enum.IsDefined(parsed). Enum.IsDefined matches declared member values, so single-flag integers (1, 2, 4, 8) are declared members of ModelModality and pass. Composite 3 is not a declared member, so it fails — which makes the rejection of 3 a coincidence of compositeness rather than the intended integer check.
The comment at ModelCommand.cs:311-312 asserts "a mistyped or scripted number is not silently coerced", which overstates the actual contract.
Why it matters
Enum.TryParse accepting numeric strings is the same class of bug that #1660 fixed for --verification-kind (where Enum.TryParse accepted "1" and could not parse header-secret). This is the surviving instance of that pattern.
The JSON schema is already stricter than the CLI — netclaw-config.v1.schema.json:876 uses ^(Text|Image|Audio|Video)(\s*,\s*(Text|Image|Audio|Video))*$, which is case-sensitive and integer-rejecting. The CLI is the outlier.
Impact is limited by normalization: values round-trip through input.ToString() (ModelEntryWriter.cs:480), so 1 lands on disk as canonical "Text" and does not corrupt config. This is a contract/validation bug, not a data-integrity bug.
Test gap
Set_InvalidOptions_ReturnErrorWithoutWriting (ModelCommandTests.cs:443) exercises only "3" — the one invalid integer that passes for the wrong reason. "1" is untested.
Suggested fix
Reject input that parses as an integer literal before calling Enum.TryParse, and add cases for 1/2/4/8 to the test above.
Docs impact
netclaw-dev/netclaw-website#83 asks docs to state that raw integers are rejected. Until this is fixed, that claim would be false, so the docs will document named flags only and stay silent on the integer path. Tracked in netclaw-dev/netclaw-website#86.
Summary
netclaw model set --input-modalities/--output-modalitiesaccept raw integers for single-flag values, contrary to the intended contract stated in the code's own comment.Repro
Verified against
netclaw 0.25.0-alpha.onnx.7 (commit 5c7278f)in an isolatedNETCLAW_HOME:Resulting definition — the integer is silently coerced to a named flag:
For contrast, these are correctly rejected:
1,2,4,8Text/Image/Audio/Video3(composite),0,99,BogusCause
ModelCommand.cs:313-315guards with!Enum.IsDefined(parsed).Enum.IsDefinedmatches declared member values, so single-flag integers (1,2,4,8) are declared members ofModelModalityand pass. Composite3is not a declared member, so it fails — which makes the rejection of3a coincidence of compositeness rather than the intended integer check.The comment at
ModelCommand.cs:311-312asserts "a mistyped or scripted number is not silently coerced", which overstates the actual contract.Why it matters
Enum.TryParseaccepting numeric strings is the same class of bug that #1660 fixed for--verification-kind(whereEnum.TryParseaccepted"1"and could not parseheader-secret). This is the surviving instance of that pattern.The JSON schema is already stricter than the CLI —
netclaw-config.v1.schema.json:876uses^(Text|Image|Audio|Video)(\s*,\s*(Text|Image|Audio|Video))*$, which is case-sensitive and integer-rejecting. The CLI is the outlier.Impact is limited by normalization: values round-trip through
input.ToString()(ModelEntryWriter.cs:480), so1lands on disk as canonical"Text"and does not corrupt config. This is a contract/validation bug, not a data-integrity bug.Test gap
Set_InvalidOptions_ReturnErrorWithoutWriting(ModelCommandTests.cs:443) exercises only"3"— the one invalid integer that passes for the wrong reason."1"is untested.Suggested fix
Reject input that parses as an integer literal before calling
Enum.TryParse, and add cases for1/2/4/8to the test above.Docs impact
netclaw-dev/netclaw-website#83 asks docs to state that raw integers are rejected. Until this is fixed, that claim would be false, so the docs will document named flags only and stay silent on the integer path. Tracked in netclaw-dev/netclaw-website#86.