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
10 changes: 10 additions & 0 deletions .fusa-reqs.json
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,16 @@
"verification": "test",
"safety_goal": "SG-006",
"asil": "ASIL-B"
},
{
"id": "REQ-RELAY-097",
"title": "multi_protocol capabilities field tightens Requirements 1 and 6",
"text": "The capabilities document (spec \u00a712.2) MAY declare an OPTIONAL boolean multi_protocol field, defaulting to false when absent. relay conform MUST FAIL a capabilities document whose protocol is null unless multi_protocol is true (spec \u00a717 Requirement 1), and MUST FAIL a capabilities document whose adapt is false unless multi_protocol is true (spec \u00a717 Requirement 6) \u2014 replacing the prior WARN-only treatment of both cases. A capabilities document with multi_protocol:true MUST NOT produce any finding for either a null protocol or adapt:false.",
"category": "functional",
"criticality": "medium",
"verification": "test",
"safety_goal": "SG-006",
"asil": "ASIL-B"
}
]
}
38 changes: 35 additions & 3 deletions cmd/relay/conform.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,10 @@ func buildManifest(binary string) conformManifest {
}

req1 := statusPass
if hasFail(vFindings) {
// Requirement 1 (protocol declaration) spans both documents: spec_version
// shape (version doc) and the capabilities doc's own protocol-null check
// (§12.2, gated on multi_protocol — see validateCapabilitiesDoc).
if hasFail(vFindings) || hasFailWithReq(cFindings, "§12.2") {
req1 = statusFail
}
req6 := statusPass
Expand Down Expand Up @@ -318,6 +321,17 @@ func hasFail(fs []conformFinding) bool {
return false
}

// hasFailWithReq reports whether any finding in fs is FAIL-severity and cites
// req (spec section) exactly.
func hasFailWithReq(fs []conformFinding, req string) bool {
for _, f := range fs {
if f.Severity == sevFail && f.Req == req {
return true
}
}
return false
}

func printConformText(w io.Writer, cr conformResult) {
for _, f := range cr.Findings {
fmt.Fprintf(w, "%-4s %s %s\n", f.Severity, f.Req, f.Message)
Expand Down Expand Up @@ -388,6 +402,7 @@ func validateVersionDoc(data []byte) []conformFinding {
//
//fusa:req REQ-RELAY-054
//fusa:req REQ-RELAY-048
//fusa:req REQ-RELAY-097
func validateCapabilitiesDoc(data []byte) []conformFinding {
doc, fs := schemaCheck("cli-capabilities", "§12.2", data)
if doc == nil {
Expand All @@ -410,9 +425,26 @@ func validateCapabilitiesDoc(data []byte) []conformFinding {
}
}

// adapt=false is valid (no Adapt() exported) but worth flagging (§10.3).
// multi_protocol (§12.2, §17 Requirements 1 and 6) self-declares that this
// binary is inherently multi-protocol tooling, not a single-protocol
// implementation. Absent/false is the default (single-protocol).
multiProtocol, _ := doc["multi_protocol"].(bool)

// A null protocol/protocol_int is only legitimate for a self-declared
// multi-protocol tool (§10.3, §17 Requirement 1); otherwise it's a real gap.
if !multiProtocol && doc["protocol"] == nil {
fs = append(fs, fail("§12.2", "capabilities doc: protocol is null but multi_protocol is not true"))
}

// adapt=false is only legitimate for a self-declared multi-protocol tool —
// §10.3 scopes Adapt() to protocol packages, so a multi-protocol aggregator
// has no per-protocol Adapt() to export (§17 Requirement 6).
if adapt, ok := doc["adapt"].(bool); ok && !adapt {
fs = append(fs, warn("§17.6", "capabilities doc: adapt=false (Adapt() not exported)"))
if multiProtocol {
// Legitimate: no per-protocol Adapt() applies to this tool.
} else {
fs = append(fs, fail("§17.6", "capabilities doc: adapt=false (Adapt() not exported) but multi_protocol is not true"))
}
}

return fs
Expand Down
64 changes: 54 additions & 10 deletions cmd/relay/conform_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,11 @@ func TestValidateVersionDocUnknownLanguage(t *testing.T) {

//fusa:test REQ-RELAY-054
func TestValidateCapabilitiesDocValid(t *testing.T) {
// No protocol declared, so this fixture must self-declare multi_protocol:
// true (spec §12.2, §17 Requirement 1) to avoid the new protocol-null FAIL.
data := []byte(`{
"kind":"capabilities","tool":"go-can","version":"1.0.0","spec_version":"0.2",
"multi_protocol":true,
"commands":["version","capabilities","status"],
"transports":[],"features":[],"interfaces":[],"optional_interfaces":[],
"adapt":true
Expand Down Expand Up @@ -163,29 +166,70 @@ func TestValidateCapabilitiesDocMissingCommand(t *testing.T) {
}

//fusa:test REQ-RELAY-054
func TestValidateCapabilitiesDocAdaptWarn(t *testing.T) {
//fusa:test REQ-RELAY-097
func TestValidateCapabilitiesDocAdaptFalseFails(t *testing.T) {
// A single-protocol implementation (multi_protocol absent/false) that
// exports no Adapt() MUST FAIL (spec §17 Requirement 6), not WARN.
data := []byte(`{
"kind":"capabilities","tool":"relay","version":"0.1.0","spec_version":"0.2",
"kind":"capabilities","tool":"go-can","protocol":"CAN","protocol_int":1,
"version":"0.1.0","spec_version":"0.2",
"commands":["version","capabilities","status"],
"transports":[],"features":[],"interfaces":[],"optional_interfaces":[],
"adapt":false
}`)
fs := validateCapabilitiesDoc(data)
hasWarn := false
hasFail := false
for _, f := range fs {
if f.Severity == sevWarn && strings.Contains(f.Message, "adapt") {
hasWarn = true
if f.Severity == sevFail && strings.Contains(f.Message, "adapt") {
hasFail = true
}
if f.Severity == sevFail {
}
if !hasFail {
t.Error("expected FAIL for adapt=false on a single-protocol tool, got none")
}
}

//fusa:test REQ-RELAY-054
//fusa:test REQ-RELAY-097
func TestValidateCapabilitiesDocNullProtocolFails(t *testing.T) {
// A single-protocol implementation (multi_protocol absent/false) with a
// null protocol MUST FAIL (spec §17 Requirement 1), not WARN.
data := []byte(`{
"kind":"capabilities","tool":"go-can","version":"0.1.0","spec_version":"0.2",
"commands":["version","capabilities","status"],
"transports":[],"features":[],"interfaces":[],"optional_interfaces":[],
"adapt":true
}`)
fs := validateCapabilitiesDoc(data)
hasFail := false
for _, f := range fs {
if f.Severity == sevFail && strings.Contains(f.Message, "protocol") {
hasFail = true
}
}
if !hasWarn {
t.Error("expected WARN for adapt=false, got none")
if !hasFail {
t.Error("expected FAIL for null protocol on a single-protocol tool, got none")
}
if hasFail {
t.Errorf("complete adapt=false doc should not FAIL: %+v", fs)
}

//fusa:test REQ-RELAY-054
//fusa:test REQ-RELAY-097
func TestValidateCapabilitiesDocMultiProtocolExempt(t *testing.T) {
// multi_protocol:true legitimizes both a null protocol and adapt:false
// (spec §12.2, §17 Requirements 1 and 6) — RELAY's own reference CLI's
// exact shape. Neither MUST produce a finding at all, not even a WARN.
data := []byte(`{
"kind":"capabilities","tool":"relay","multi_protocol":true,
"version":"0.1.0","spec_version":"0.2",
"commands":["version","capabilities","status"],
"transports":[],"features":[],"interfaces":[],"optional_interfaces":[],
"adapt":false
}`)
fs := validateCapabilitiesDoc(data)
for _, f := range fs {
if f.Severity != sevPass {
t.Errorf("multi_protocol:true doc must produce no FAIL/WARN for protocol/adapt, got %s %s: %s", f.Severity, f.Req, f.Message)
}
}
}

Expand Down
7 changes: 5 additions & 2 deletions cmd/relay/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
relay "github.com/SoundMatt/RELAY/v2"
)

const toolVersion = "2.4.0"
const toolVersion = "2.5.0"

func main() {
if err := run(os.Stdout, os.Stderr, os.Args[1:]); err != nil {
Expand Down Expand Up @@ -141,13 +141,15 @@ func runVersion(w io.Writer, args []string) error {

// runCapabilities implements `relay capabilities`.
// RELAY is a multi-protocol spec and tooling layer, not a protocol implementation,
// so protocol and protocol_int are omitted and adapt is false.
// so protocol and protocol_int are omitted, adapt is false, and multi_protocol
// is declared true — legitimizing both per spec §12.2 / §17 Requirements 1 and 6.
//
//fusa:req REQ-RELAY-029
func runCapabilities(w io.Writer, _ []string) error {
doc := struct {
Kind string `json:"kind"`
Tool string `json:"tool"`
MultiProtocol bool `json:"multi_protocol"`
Version string `json:"version"`
SpecVersion string `json:"spec_version"`
Commands []string `json:"commands"`
Expand All @@ -159,6 +161,7 @@ func runCapabilities(w io.Writer, _ []string) error {
}{
Kind: "capabilities",
Tool: "relay",
MultiProtocol: true,
Version: toolVersion,
SpecVersion: relay.SpecVersion,
Commands: []string{"version", "capabilities", "status", "conform", "convert", "interop", "crossbar", "probe", "trace", "report", "sbom", "safety-case", "audit-pack", "compare", "versions", "serve"},
Expand Down
39 changes: 39 additions & 0 deletions spec/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,44 @@
# RELAY Spec Changelog

## v2.5 — 2026-08-21 (MINOR — new optional capabilities field, tightened §17 Requirements 1 and 6)

- **New optional `multi_protocol` capabilities field (§12.2).** Defaults to
`false` when absent. A tool that self-declares `multi_protocol: true`
legitimately reports a null `protocol`/`protocol_int` and `adapt: false`:
§10.3 scopes the `Adapt()` contract to protocol packages, so a
multi-protocol aggregator (like RELAY's own reference CLI) has no single
protocol to declare or per-protocol adapter to export.
- **§17 Requirements 1 and 6 tightened from WARN to FAIL.** A null
`protocol`/`protocol_int`, or `adapt: false`, on a capabilities document
that does not declare `multi_protocol: true` is now a conformance FAIL,
closing a real audit-flagged gap (THEME-B, capabilities drifting silently
from the shipped binary) — previously both were only WARN, so an
implementation using `relay conform` without `--strict` never failed on
either. Both requirements move from "Partial" to "Full" black-box coverage
in the requirement-to-verifier table.
- **Scope decision**: the originating issue also proposed verifying every
declared `commands` string is actually invocable, and "exercising" every
declared `features` string with a CLI probe. Both are explicitly declined
in this release: there is no existing, spec-grounded signal distinct from
the generic "invalid arguments" exit code (§11.3) to detect an unrecognized
command across four languages' worth of implementations, and §12.2 already
states `features` are compiled-in and explicitly not runtime-probed. Adding
either would mean inventing an unproven new convention under this issue's
scope rather than tightening an existing one — deferred to a dedicated
follow-up.
- **Design note**: the naive implementation (blindly turning both WARN cases
into FAIL) would have broken RELAY's own reference CLI's passing
self-conformance CI job, which is deliberately, legitimately
multi-protocol and non-adapting. `multi_protocol` exists specifically to
let `relay conform`'s black-box CLI distinguish that legitimate case from
a genuine single-protocol implementation bug, which it previously could
not do at all.
- **Reference implementation**: `cmd/relay`'s own `capabilities` output now
declares `"multi_protocol": true`; `validateCapabilitiesDoc` in
`cmd/relay/conform.go` implements the gated FAIL logic for both fields.
New `REQ-RELAY-097`. `SpecVersion` bumped to `2.5`. Closes [NEW-SPEC-3]
(partial — commands-invocability and feature-probing deferred).

## v2.4 — 2026-08-21 (MINOR — new §17 conformance requirement)

- **New §17 Requirement 16 — Vector manifest.** The canonical
Expand Down
Loading
Loading