Skip to content

Commit 43d90fd

Browse files
committed
Add controlled-session framed protocol
Define shared lifecycle result and status types, then add the versioned typed frame codec used between the host supervisor and controller. Preserve terminal bytes exactly while validating structured event payloads, frame direction, dimensions, endpoint identifiers, result semantics, duplicate JSON fields, protocol version, and bounded payload length. Add round-trip, malformed-frame, direction-confusion, duplicate-field, binary-data, and fuzz-seed coverage.
1 parent 6733fd5 commit 43d90fd

3 files changed

Lines changed: 891 additions & 0 deletions

File tree

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package controlledsession
2+
3+
import "fmt"
4+
5+
type TerminationCauseV1 string
6+
7+
const (
8+
CauseControllerCompleteV1 TerminationCauseV1 = "controller-complete"
9+
CauseControllerTerminateV1 TerminationCauseV1 = "controller-terminate"
10+
CauseApplicationExitV1 TerminationCauseV1 = "application-exit"
11+
CauseHostCancelV1 TerminationCauseV1 = "host-cancel"
12+
CauseControllerLostV1 TerminationCauseV1 = "controller-lost"
13+
CauseRuntimeObservationLostV1 TerminationCauseV1 = "runtime-observation-lost"
14+
CauseStartupFailureV1 TerminationCauseV1 = "startup-failure"
15+
)
16+
17+
type ProcessStatusKindV1 string
18+
19+
const (
20+
ProcessStatusUnknownV1 ProcessStatusKindV1 = "unknown"
21+
ProcessStatusExitedV1 ProcessStatusKindV1 = "exited"
22+
ProcessStatusTerminatedV1 ProcessStatusKindV1 = "terminated"
23+
ProcessStatusUnavailableV1 ProcessStatusKindV1 = "unavailable"
24+
)
25+
26+
type ProcessStatusV1 struct {
27+
Kind ProcessStatusKindV1 `json:"kind"`
28+
Code *int `json:"code,omitempty"`
29+
Reason string `json:"reason,omitempty"`
30+
}
31+
32+
type ControllerStatusKindV1 string
33+
34+
const (
35+
ControllerStatusUnknownV1 ControllerStatusKindV1 = "unknown"
36+
ControllerStatusActiveV1 ControllerStatusKindV1 = "active"
37+
ControllerStatusCompletedV1 ControllerStatusKindV1 = "completed"
38+
ControllerStatusLostV1 ControllerStatusKindV1 = "lost"
39+
ControllerStatusFinalizationTimeoutV1 ControllerStatusKindV1 = "finalization-timeout"
40+
ControllerStatusStoppedV1 ControllerStatusKindV1 = "stopped"
41+
ControllerStatusStartupFailedV1 ControllerStatusKindV1 = "startup-failed"
42+
)
43+
44+
type ControllerStatusV1 struct {
45+
Kind ControllerStatusKindV1 `json:"kind"`
46+
Reason string `json:"reason,omitempty"`
47+
}
48+
49+
type CleanupStatusKindV1 string
50+
51+
const (
52+
CleanupStatusSucceededV1 CleanupStatusKindV1 = "succeeded"
53+
CleanupStatusFailedV1 CleanupStatusKindV1 = "failed"
54+
)
55+
56+
type CleanupStatusV1 struct {
57+
Kind CleanupStatusKindV1 `json:"kind"`
58+
Message string `json:"message,omitempty"`
59+
}
60+
61+
type RecoveryActionV1 string
62+
63+
const (
64+
RecoveryNoneV1 RecoveryActionV1 = "none"
65+
RecoveryRetryCleanupV1 RecoveryActionV1 = "retry-cleanup"
66+
RecoveryReconcileNextOperationV1 RecoveryActionV1 = "reconcile-next-operation"
67+
)
68+
69+
type ResultV1 struct {
70+
Cause TerminationCauseV1 `json:"cause"`
71+
ApplicationStatus ProcessStatusV1 `json:"application_status"`
72+
ControllerStatus ControllerStatusV1 `json:"controller_status"`
73+
CleanupStatus CleanupStatusV1 `json:"cleanup_status"`
74+
RecoveryAction RecoveryActionV1 `json:"recovery_action"`
75+
}
76+
77+
func ValidateResultV1(result ResultV1) error {
78+
if !validTerminationCauseV1(result.Cause) {
79+
return fmt.Errorf("controlled-session termination cause %q is invalid", result.Cause)
80+
}
81+
if err := validateProcessStatusV1(result.ApplicationStatus, true); err != nil {
82+
return err
83+
}
84+
if err := validateTerminalControllerStatusV1(result.ControllerStatus); err != nil {
85+
return err
86+
}
87+
return validateCleanupResultV1(result.CleanupStatus, result.RecoveryAction)
88+
}
89+
90+
func validateProcessStatusV1(status ProcessStatusV1, allowUnknown bool) error {
91+
switch status.Kind {
92+
case ProcessStatusUnknownV1:
93+
if !allowUnknown || status.Code != nil || status.Reason != "" {
94+
return fmt.Errorf("unknown application status must have no code or reason")
95+
}
96+
case ProcessStatusExitedV1:
97+
if status.Code == nil {
98+
return fmt.Errorf("exited application status requires an exit code")
99+
}
100+
case ProcessStatusTerminatedV1, ProcessStatusUnavailableV1:
101+
if status.Code != nil {
102+
return fmt.Errorf("application status %q must not contain an exit code", status.Kind)
103+
}
104+
default:
105+
return fmt.Errorf("application status %q is invalid", status.Kind)
106+
}
107+
return validateOptionalSafeTextV1("application status reason", status.Reason)
108+
}
109+
110+
func validateTerminalControllerStatusV1(status ControllerStatusV1) error {
111+
switch status.Kind {
112+
case ControllerStatusCompletedV1, ControllerStatusLostV1, ControllerStatusFinalizationTimeoutV1, ControllerStatusStoppedV1, ControllerStatusStartupFailedV1:
113+
default:
114+
return fmt.Errorf("terminal controller status %q is invalid", status.Kind)
115+
}
116+
return validateOptionalSafeTextV1("controller status reason", status.Reason)
117+
}
118+
119+
func validateCleanupResultV1(status CleanupStatusV1, recovery RecoveryActionV1) error {
120+
switch status.Kind {
121+
case CleanupStatusSucceededV1:
122+
if status.Message != "" {
123+
return fmt.Errorf("successful cleanup status must not contain an error message")
124+
}
125+
if recovery != RecoveryNoneV1 {
126+
return fmt.Errorf("successful cleanup must not require recovery")
127+
}
128+
case CleanupStatusFailedV1:
129+
if err := validateRequiredSafeTextV1("cleanup failure message", status.Message); err != nil {
130+
return err
131+
}
132+
if recovery != RecoveryRetryCleanupV1 && recovery != RecoveryReconcileNextOperationV1 {
133+
return fmt.Errorf("failed cleanup requires a recovery action")
134+
}
135+
default:
136+
return fmt.Errorf("cleanup status %q is invalid", status.Kind)
137+
}
138+
return nil
139+
}
140+
141+
func validateOptionalSafeTextV1(field string, value string) error {
142+
if value == "" {
143+
return nil
144+
}
145+
return validateSafeTextV1(field, value)
146+
}
147+
148+
func validateRequiredSafeTextV1(field string, value string) error {
149+
if value == "" {
150+
return fmt.Errorf("controlled-session %s is required", field)
151+
}
152+
return validateSafeTextV1(field, value)
153+
}
154+
155+
func validTerminationCauseV1(cause TerminationCauseV1) bool {
156+
switch cause {
157+
case CauseControllerCompleteV1, CauseControllerTerminateV1, CauseApplicationExitV1, CauseHostCancelV1,
158+
CauseControllerLostV1, CauseRuntimeObservationLostV1, CauseStartupFailureV1:
159+
return true
160+
default:
161+
return false
162+
}
163+
}

0 commit comments

Comments
 (0)