Fail closed on Nexus/Hexis dependency errors, accept flat resolve shape

Vikunja #268 (P0): handleHexisAct swallowed genuine Nexus resolve errors
and Hexis capability-discovery errors into "" or an empty capability list,
which fell through to the local system command executor — a dependency
outage silently looked identical to "not an ecosystem entity" or "no
capabilities registered", violating the spec's degrade-independently /
never-silent-all-clear invariant.

- resolveEntityReference's error is now distinguished from a legitimate
  not_found: only the latter falls through.
- discoverCapabilities now returns (caps, err) instead of collapsing a
  Hexis failure into an empty slice; a real error stops the action with
  a degraded-mode spoken reply instead of reaching h.tools.Exec.
- nexusResolveResult gains a custom UnmarshalJSON to accept the flat
  entity_id/entity_type/display_name shape from ECOSYSTEM-SPEC.md §1.5
  (Nexus now emits both shapes; Maven now reads both).
- Added regression tests: flat-shape resolve, Nexus error fails closed,
  Hexis error fails closed, not_found still falls through to local exec.
This commit is contained in:
kami
2026-07-20 01:08:11 +04:00
parent 44421d2048
commit d9fa4d6613
3 changed files with 141 additions and 9 deletions
+13 -4
View File
@@ -56,6 +56,7 @@ import (
"sync"
"time"
hexisclient "github.com/kami/hexis/pkg/client"
"github.com/kami/maven/internal/audio"
"github.com/kami/maven/internal/config"
"github.com/kami/maven/internal/delivery"
@@ -75,7 +76,6 @@ import (
"github.com/kami/maven/internal/voice"
"github.com/kami/maven/internal/weather"
"github.com/kami/maven/internal/worker"
hexisclient "github.com/kami/hexis/pkg/client"
)
// voiceWiring — everything the daemon needs to run the audio path. Held by
@@ -1229,7 +1229,11 @@ func (h *reactiveHandler) handleHexisAct(ctx context.Context, dec router.Decisio
// ambiguous match must stop and clarify — never guess a mutation target.
entityID, displayName, ambiguous, err := h.ecosystem.resolveEntityReference(ctx, dec.Slots.Text, nil)
if err != nil {
return ""
// A genuine Nexus dependency failure, not "no such entity" — stop here
// and report degradation rather than silently falling through to the
// local command executor (ECOSYSTEM-SPEC.md: services degrade
// independently, never a silent all-clear).
return "экосистема недоступна, попробуй ещё раз."
}
if len(ambiguous) > 0 {
return "уточни, что именно: " + strings.Join(ambiguous, ", ") + "?"
@@ -1238,8 +1242,13 @@ func (h *reactiveHandler) handleHexisAct(ctx context.Context, dec router.Decisio
return ""
}
// Discover Hexis capabilities for this entity.
caps := h.ecosystem.discoverCapabilities(ctx, entityID)
// Discover Hexis capabilities for this entity. A resolved entity with a
// genuine Hexis failure must not be treated as "no capabilities" and
// fall through to unrelated local execution.
caps, err := h.ecosystem.discoverCapabilities(ctx, entityID)
if err != nil {
return "экосистема недоступна, попробуй ещё раз."
}
if len(caps) == 0 {
return ""
}