Files
zero/internal/selfverify/contracts.go
KRATOSandKRATOS 34ad21a551 Add runtime permission and verification event contracts (#82)
* feat: add runtime permission event contract

* feat: stream permission events from exec

* feat: add runtime command snapshots

* feat: add session rewind compaction plans

* feat: add verification event contracts

* fix: harden runtime contract review gaps

* test: assert approved exec stderr contract

* fix: address CodeRabbit CHANGES_REQUESTED items for #82

- Make OnSandboxDecision callback async + panic-safe (non-blocking, recovered) in tools/registry.go per CR.
- Add explicit regression test for ConfigSnapshot when ResolveRuntimeMetadata errors (no secret leak in Message/BaseURL).
- (Many prior items like shapedPayloadPreview for permission/tool events, dual-target conflict test, stderr empty on approved path, Kind/Framework preservation, redactCommand empty slice, etc. were already landed on this branch.)

All relevant tests now pass.

* fix(agent): make sandbox decision available synchronously on Result for permission events

The recent change to invoke OnSandboxDecision asynchronously (to satisfy non-blocking requirement) broke the agent's immediate capture of the decision for building PermissionEvent after registry.RunWithOptions.

- Add SandboxDecision *sandbox.Decision to tools.Result (not serialized).
- Populate it on all sandbox-involved return paths in registry.RunWithOptions (sync, before/while firing async callback for observers).
- In agent executeToolCall, use result.SandboxDecision for buildPermissionEvent (reliable ordering) instead of side-effect capture via the callback.
- The async callback is still supported for any external OnSandboxDecision consumers.

This fixes the two failing agent tests that were causing the cross-platform 'Smoke' CI jobs to fail on this PR:
- TestRunEmitsPermissionEventForPersistentSandboxGrant
- TestRunAppliesSandboxEvenInUnsafeMode

All go tests now pass.

---------

Co-authored-by: KRATOS <[email protected]>
2026-06-06 13:13:08 +05:30

140 lines
4.5 KiB
Go

package selfverify
import (
"github.com/Gitlawb/zero/internal/redaction"
"github.com/Gitlawb/zero/internal/verify"
)
const LoopContractVersion = "zero.selfverify.report.v1"
type EventType string
const (
EventLoopStarted EventType = "selfverify_started"
EventAttemptFinished EventType = "selfverify_attempt_finished"
EventLoopFinished EventType = "selfverify_finished"
)
type LoopSnapshot struct {
Contract string `json:"contract"`
Runtime string `json:"runtime"`
Root string `json:"root,omitempty"`
StartedAt string `json:"startedAt"`
EndedAt string `json:"endedAt"`
OK bool `json:"ok"`
StopReason StopReason `json:"stopReason"`
Summary verify.Summary `json:"summary"`
Attempts []AttemptSnapshot `json:"attempts"`
Error string `json:"error,omitempty"`
Events []Event `json:"events"`
}
type AttemptSnapshot struct {
Number int `json:"number"`
Report verify.ReportSnapshot `json:"report"`
Remediation *Remediation `json:"remediation,omitempty"`
}
type Event struct {
Type EventType `json:"type"`
Runtime string `json:"runtime,omitempty"`
Contract string `json:"contract,omitempty"`
Root string `json:"root,omitempty"`
Attempt int `json:"attempt,omitempty"`
Status verify.Status `json:"status,omitempty"`
OK bool `json:"ok,omitempty"`
StopReason StopReason `json:"stopReason,omitempty"`
Summary verify.Summary `json:"summary,omitempty"`
StartedAt string `json:"startedAt,omitempty"`
EndedAt string `json:"endedAt,omitempty"`
Error string `json:"error,omitempty"`
Remediation *Remediation `json:"remediation,omitempty"`
}
func SnapshotFromReport(report Report) LoopSnapshot {
attempts := make([]AttemptSnapshot, 0, len(report.Attempts))
for _, attempt := range report.Attempts {
attempts = append(attempts, AttemptSnapshot{
Number: attempt.Number,
Report: verify.SnapshotFromReport(attempt.Report),
Remediation: redactRemediationPointer(attempt.Remediation),
})
}
return LoopSnapshot{
Contract: LoopContractVersion,
Runtime: verify.RuntimeGo,
Root: redactContractString(report.Root),
StartedAt: redactContractString(report.StartedAt),
EndedAt: redactContractString(report.EndedAt),
OK: report.OK,
StopReason: report.StopReason,
Summary: report.Summary,
Attempts: attempts,
Error: redactContractString(report.Error),
Events: EventsFromReport(report),
}
}
func EventsFromReport(report Report) []Event {
events := []Event{
{
Type: EventLoopStarted,
Runtime: verify.RuntimeGo,
Contract: LoopContractVersion,
Root: redactContractString(report.Root),
StartedAt: redactContractString(report.StartedAt),
Summary: report.Summary,
},
}
for _, attempt := range report.Attempts {
status := verify.StatusFail
if attempt.Report.OK {
status = verify.StatusPass
} else if attempt.Report.Summary.Errors > 0 {
status = verify.StatusError
}
events = append(events, Event{
Type: EventAttemptFinished,
Runtime: verify.RuntimeGo,
Contract: LoopContractVersion,
Root: redactContractString(report.Root),
Attempt: attempt.Number,
Status: status,
OK: attempt.Report.OK,
Summary: attempt.Report.Summary,
StartedAt: redactContractString(attempt.Report.StartedAt),
EndedAt: redactContractString(attempt.Report.EndedAt),
Remediation: redactRemediationPointer(attempt.Remediation),
})
}
events = append(events, Event{
Type: EventLoopFinished,
Runtime: verify.RuntimeGo,
Contract: LoopContractVersion,
Root: redactContractString(report.Root),
OK: report.OK,
StopReason: report.StopReason,
Summary: report.Summary,
StartedAt: redactContractString(report.StartedAt),
EndedAt: redactContractString(report.EndedAt),
Error: redactContractString(report.Error),
})
return events
}
func redactRemediationPointer(remediation *Remediation) *Remediation {
if remediation == nil {
return nil
}
next := *remediation
next.StartedAt = redactContractString(next.StartedAt)
next.EndedAt = redactContractString(next.EndedAt)
next.Message = redactContractString(next.Message)
next.Error = redactContractString(next.Error)
return &next
}
func redactContractString(value string) string {
return redaction.RedactString(value, redaction.Options{})
}