Docker adapters restamp relationship ObservedAt/LastSeenAt on every
~15s report, and the action planner folded those stamps into the plan's
resource version, so any reviewed action against a relationship-bearing
container (start, stop, restart, and the restored update) drifted to a
409 action_plan_drift before a human could read the review dialog and
click approve. Relationship edges now count by identity (source,
target, type, active, discoverer, metadata), the same
identity-versus-timestamp boundary change emission drew for issue
#1496. Found live: the UI update journey failed with plan drift on
every attempt slower than one report cycle.
Privacy: provider-streamed RawInput overrides on tool_progress events
are unredacted model output; for exposure-restricted tools they are now
discarded instead of replacing the projected form (the override was
reintroducing exactly the values the projector removed). Proven with a
progress event carrying a secret in the raw override.
Schema validation: proposal acceptance now validates through the
planner's exported canonical rules - FindCapability's exact-name
matching (the capture previously matched case-insensitively while
planning matched exactly) and ValidateParams for declared, required,
typed, enum, pattern, and malformed-schema cases - so a proposal that
validates is exactly a proposal the planner will accept. The
sensitive-parameter rejection remains a proposal-specific ratchet on
top.
Fail-closed ratchets: params and evidence identity are deep-cloned on
capture and again on outcome, so caller-side mutation after validation
can never alter the actionable proposal; fingerprint serialization
failures return errors rather than a shared sentinel value; an
investigation run refuses to start without finding and investigation
identity (before any provider call or session exists); and any run
error nils the proposal while preserving simultaneous proposal errors
via errors.Join - a non-nil proposal exists only from a completely
successful run.
The AI action broker treated an unreadable operator lock as unlocked:
isResourceRemediationLocked returned (false, nil) with no audit store
wired, and the caller logged store errors then dispatched anyway. An
operator's NeverAutoRemediate=true could be silently ignored whenever
the policy store was missing or erroring, which is unacceptable while
Patrol and Assistant run at assisted or full autonomy.
Posture change at the dispatch decision point:
- isResourceRemediationLocked now reports unknown state (nil store or
lookup failure) as an ErrRemediationLockStateUnknown-wrapped error
instead of silently defaulting to unlocked.
- New checkRemediationLockForDispatch gate: dispatches without an
approved human decision fail CLOSED on unknown lock state and
surface "remediation lock state unknown; operator approval
required". Human-approved dispatches keep the historical fail-open
behavior with a warning log. A confirmed lock still refuses even
approved dispatches, as before.
- executeNativeActionWithAudit (TrueNAS app start/stop/restart) now
enforces the lock too; it previously skipped the check entirely.
- Refusals persist Failed audit records with stable
remediation_lock_state_unknown: / resource_remediation_locked:
ErrorMessage prefixes.
- ai-runtime subsystem contract updated to pin the new posture.
Tests cover store-error and nil-store at both autonomy postures on
both dispatch paths; routing/control tests now wire an in-memory
audit store since autonomous dispatch without one is refused.