Require auth on /api/v1/ and derive capability guards server-side

Findings 1 and 2 of REVIEW-2026-07-30.md, which must land together: every
workspace capability registered with enabled=false, so the only working
provider could never execute. Fixing that alone would have turned a dead
execution path into a reachable one on an unauthenticated port.

Auth: a shared bearer token (HEXIS_API_TOKEN) is now required on the whole
/api/v1/ surface, compared with crypto/subtle.ConstantTimeCompare. /health
and /ready stay open for probes. It fails closed twice over — hexisd refuses
to start with an empty token, and the middleware returns 503 rather than ever
serving unauthenticated.

Guards: `enabled` and `requires_confirmation` are no longer readable from the
request body at all. Previously the handler derived the correct §4.3 default
and then let the caller override it, which is worse than no guard because it
reads as enforced. Both are now derived from the risk tier by shared helpers
in domain, used by the HTTP and provider registration paths alike;
unrecognised tiers fail closed to requiring confirmation.

BuildCapabilities sets Enabled, RequiresConfirmation and TimeoutSeconds
explicitly, and hexisd reconciles drifted rows on startup instead of skipping
any capability whose ID already exists — without that, allowlist edits never
reach an existing database.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uea55zaiWuEByEDC4UBSdd
This commit is contained in:
kami
2026-07-30 23:39:13 +04:00
parent 945e4ba1ac
commit c7325a20d4
7 changed files with 368 additions and 90 deletions
+34 -4
View File
@@ -36,10 +36,40 @@ type Capability struct {
Version int64 `json:"version"`
}
// IsDestructive reports whether the capability's risk tier requires it to be
// disabled by default per ECOSYSTEM-SPEC.md §4.3.
func (c *Capability) IsDestructive() bool {
return c.Risk == "destructive"
// Risk tiers. ECOSYSTEM-SPEC.md §4.1 names read/low/protected/destructive; the
// workspace allowlist additionally uses medium/high, which map onto the same
// two guard decisions below.
const (
RiskRead = "read"
RiskLow = "low"
RiskMedium = "medium"
RiskProtected = "protected"
RiskHigh = "high"
RiskDestructive = "destructive"
)
// DefaultCapabilityTimeoutSeconds is the per-capability wall-clock timeout
// applied when a registration does not specify one (ECOSYSTEM-SPEC.md §4.3).
const DefaultCapabilityTimeoutSeconds = 30
// EnabledForRisk reports the server-derived `enabled` value for a risk tier.
// Destructive capabilities are disabled by default and must be turned on
// explicitly out of band (ECOSYSTEM-SPEC.md §4.3). This is never overridable
// by an API caller.
func EnabledForRisk(risk string) bool {
return risk != RiskDestructive
}
// RequiresConfirmationForRisk reports the server-derived
// `requires_confirmation` value for a risk tier. Only genuinely read-only and
// low-risk tiers skip confirmation; unrecognised tiers fail closed.
func RequiresConfirmationForRisk(risk string) bool {
switch risk {
case "", RiskRead, RiskLow:
return false
default:
return true
}
}
type ExecutionStatus string