mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
711 lines
31 KiB
Go
711 lines
31 KiB
Go
package agentexec
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// MessageType identifies the type of WebSocket message
|
|
type MessageType string
|
|
|
|
const (
|
|
RuntimeRoleActionRunner = "action-runner"
|
|
RuntimeRoleLegacyFullTrust = "legacy-full-trust"
|
|
ActionCapabilityTypedV1 = "typed_actions.v1"
|
|
|
|
// Agent -> Server messages
|
|
MsgTypeAgentRegister MessageType = "agent_register"
|
|
MsgTypeAgentPing MessageType = "agent_ping"
|
|
MsgTypeCommandResult MessageType = "command_result"
|
|
MsgTypeHostStorageCleanupResult MessageType = "host_storage_cleanup_result"
|
|
MsgTypeHostUpdateResult MessageType = "host_update_result"
|
|
MsgTypeProxmoxGuestLifecycleResult MessageType = "proxmox_guest_lifecycle_result"
|
|
MsgTypeDockerContainerLifecycleResult MessageType = "docker_container_lifecycle_result"
|
|
MsgTypeDockerContainerUpdateResult MessageType = "docker_container_update_result"
|
|
MsgTypeDockerContainerObserveResult MessageType = "docker_container_observe_result"
|
|
MsgTypeActionPreflightResult MessageType = "action_preflight_result"
|
|
MsgTypeOperationQueryResult MessageType = "agent_operation_query_result"
|
|
|
|
// Server -> Agent messages
|
|
MsgTypeRegistered MessageType = "registered"
|
|
MsgTypePong MessageType = "pong"
|
|
MsgTypeExecuteCmd MessageType = "execute_command"
|
|
MsgTypeHostStorageCleanup MessageType = "host_storage_cleanup"
|
|
MsgTypeReadFile MessageType = "read_file"
|
|
MsgTypeHostUpdate MessageType = "host_update"
|
|
MsgTypeProxmoxGuestLifecycle MessageType = "proxmox_guest_lifecycle"
|
|
MsgTypeDockerContainerLifecycle MessageType = "docker_container_lifecycle"
|
|
MsgTypeDockerContainerUpdate MessageType = "docker_container_update"
|
|
MsgTypeDockerContainerObserve MessageType = "docker_container_observe"
|
|
MsgTypeActionPreflight MessageType = "action_preflight"
|
|
MsgTypeOperationQuery MessageType = "agent_operation_query"
|
|
MsgTypeDeployPreflight MessageType = "deploy_preflight"
|
|
MsgTypeDeployInstall MessageType = "deploy_install"
|
|
MsgTypeDeployCancelJob MessageType = "deploy_cancel"
|
|
MsgTypeCancelCmd MessageType = "cancel_command"
|
|
|
|
// Agent -> Server messages (deploy)
|
|
MsgTypeDeployProgress MessageType = "deploy_progress"
|
|
)
|
|
|
|
// Message is the envelope for all WebSocket messages
|
|
type Message struct {
|
|
Type MessageType `json:"type"`
|
|
ID string `json:"id,omitempty"` // Unique message ID for request/response correlation
|
|
Timestamp time.Time `json:"timestamp"`
|
|
Payload json.RawMessage `json:"payload,omitempty"`
|
|
}
|
|
|
|
// NewMessage creates a message envelope with a safely marshaled payload.
|
|
func NewMessage(messageType MessageType, id string, payload any) (Message, error) {
|
|
msg := Message{
|
|
Type: messageType,
|
|
ID: id,
|
|
Timestamp: time.Now(),
|
|
}
|
|
if err := msg.SetPayload(payload); err != nil {
|
|
return Message{}, err
|
|
}
|
|
return msg, nil
|
|
}
|
|
|
|
// SetPayload marshals and sets the payload for this message.
|
|
func (m *Message) SetPayload(payload any) error {
|
|
if payload == nil {
|
|
m.Payload = nil
|
|
return nil
|
|
}
|
|
|
|
encoded, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m.Payload = encoded
|
|
return nil
|
|
}
|
|
|
|
// DecodePayload unmarshals the message payload into target.
|
|
func (m Message) DecodePayload(target any) error {
|
|
if len(m.Payload) == 0 {
|
|
return errors.New("message payload is empty")
|
|
}
|
|
return json.Unmarshal(m.Payload, target)
|
|
}
|
|
|
|
// AgentRegisterPayload is sent by agent on connection
|
|
type AgentRegisterPayload struct {
|
|
AgentID string `json:"agent_id"`
|
|
Hostname string `json:"hostname"`
|
|
Version string `json:"version"`
|
|
Platform string `json:"platform"` // "linux", "windows", "darwin"
|
|
Tags []string `json:"tags,omitempty"`
|
|
Token string `json:"token"` // API token for authentication
|
|
RuntimeRole string `json:"runtime_role,omitempty"`
|
|
ActionCapability string `json:"action_capability,omitempty"`
|
|
OperationReceiptVersion int `json:"operation_receipt_version,omitempty"`
|
|
ActionPreflightVersion int `json:"action_preflight_version,omitempty"`
|
|
DockerObservationVersion int `json:"docker_observation_version,omitempty"`
|
|
}
|
|
|
|
// RegisteredPayload is sent by server after successful registration
|
|
type RegisteredPayload struct {
|
|
Success bool `json:"success"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
// CancelCommandPayload is sent by the server when it stops waiting for a
|
|
// previously dispatched execute_command request (caller context expired or
|
|
// the server-side timeout fired). Agents that understand it abort the running
|
|
// command and reap its whole process tree; older agents ignore the message
|
|
// and rely on their own per-command timeout.
|
|
type CancelCommandPayload struct {
|
|
RequestID string `json:"request_id"`
|
|
}
|
|
|
|
// ExecuteCommandPayload is sent by server to request command execution
|
|
type ExecuteCommandPayload struct {
|
|
RequestID string `json:"request_id"`
|
|
Command string `json:"command"`
|
|
ApprovalID string `json:"approval_id,omitempty"`
|
|
ApprovalGrant *CommandApprovalGrant `json:"approval_grant,omitempty"`
|
|
TargetType string `json:"target_type"` // "agent", "container", "vm"
|
|
TargetID string `json:"target_id,omitempty"` // VMID for container/VM
|
|
Timeout int `json:"timeout,omitempty"` // seconds, 0 = default
|
|
// Trusted marks a payload as originating from a Pulse-internal subsystem
|
|
// whose command catalog is hardcoded and vetted (e.g. servicediscovery
|
|
// deep scans wrap read-only inspections in `docker exec`). When set, the
|
|
// server's command policy approval gate is bypassed for this command.
|
|
// This field must only be set by trusted internal call sites — never
|
|
// from a deserialised HTTP body or any user-driven path.
|
|
Trusted bool `json:"trusted,omitempty"`
|
|
|
|
authorization *commandAuthorizationContext
|
|
}
|
|
|
|
type commandAuthorizationContext struct {
|
|
OrgID string
|
|
ActionID string
|
|
}
|
|
|
|
// BindCommandAuthorization attaches server-owned approval scope to a command.
|
|
// The scope is deliberately not serializable, so provider arguments, HTTP
|
|
// payloads, relay callers, and agent wire messages cannot manufacture it.
|
|
func (p *ExecuteCommandPayload) BindCommandAuthorization(orgID, actionID string) {
|
|
if p == nil {
|
|
return
|
|
}
|
|
p.authorization = &commandAuthorizationContext{
|
|
OrgID: strings.TrimSpace(orgID),
|
|
ActionID: strings.TrimSpace(actionID),
|
|
}
|
|
}
|
|
|
|
// ReadFilePayload is sent by server to request file content
|
|
type ReadFilePayload struct {
|
|
RequestID string `json:"request_id"`
|
|
Path string `json:"path"`
|
|
TargetType string `json:"target_type"`
|
|
TargetID string `json:"target_id,omitempty"`
|
|
MaxBytes int64 `json:"max_bytes,omitempty"` // 0 = default (1MB)
|
|
}
|
|
|
|
// CommandResultPayload is sent by agent with command execution result
|
|
type CommandResultPayload struct {
|
|
RequestID string `json:"request_id"`
|
|
Success bool `json:"success"`
|
|
Stdout string `json:"stdout,omitempty"`
|
|
Stderr string `json:"stderr,omitempty"`
|
|
ExitCode int `json:"exit_code"`
|
|
Error string `json:"error,omitempty"`
|
|
Duration int64 `json:"duration_ms"`
|
|
}
|
|
|
|
const HostUpdateOperationInstall = "install_os_updates"
|
|
|
|
const (
|
|
DockerContainerOperationStart = "start_container"
|
|
DockerContainerOperationStop = "stop_container"
|
|
DockerContainerOperationRestart = "restart_container"
|
|
DockerContainerOperationUpdate = "update_container"
|
|
)
|
|
|
|
// DockerContainerLifecyclePayload is a closed container lifecycle operation.
|
|
// It carries an immutable container ID and request-bound before state, never
|
|
// command text, flags, a shell fragment, or a user-controlled container name.
|
|
type DockerContainerLifecyclePayload struct {
|
|
RequestID string `json:"request_id"`
|
|
ActionID string `json:"action_id"`
|
|
Operation string `json:"operation"`
|
|
OperationVersion int `json:"operation_version"`
|
|
RequestDigest string `json:"request_digest"`
|
|
Runtime string `json:"runtime"`
|
|
ContainerID string `json:"container_id"`
|
|
ExpectedState string `json:"expected_state"`
|
|
ExpectedStartedAt time.Time `json:"expected_started_at,omitempty"`
|
|
Timeout int `json:"timeout,omitempty"`
|
|
}
|
|
|
|
type DockerContainerLifecycleSnapshot struct {
|
|
ContainerID string `json:"container_id"`
|
|
State string `json:"state"`
|
|
Running bool `json:"running"`
|
|
Health string `json:"-"`
|
|
StartedAt time.Time `json:"started_at,omitempty"`
|
|
RestartCount int `json:"restart_count"`
|
|
ObservedAt time.Time `json:"observed_at"`
|
|
}
|
|
|
|
// DockerContainerObservationSnapshot is the protocol-v2, read-only daemon
|
|
// fact envelope. Health is deliberately absent from the version-1 lifecycle
|
|
// receipt above so a new agent remains receipt-compatible with an older Pulse
|
|
// server while independent verification can require an explicit health fact.
|
|
type DockerContainerObservationSnapshot struct {
|
|
ContainerID string `json:"container_id"`
|
|
State string `json:"state"`
|
|
Running bool `json:"running"`
|
|
Health string `json:"health"`
|
|
StartedAt time.Time `json:"started_at,omitempty"`
|
|
RestartCount int `json:"restart_count"`
|
|
ObservedAt time.Time `json:"observed_at"`
|
|
}
|
|
|
|
// DockerContainerObservationPayload is a server-initiated, read-only daemon
|
|
// observation made after a typed mutation has returned. It is deliberately a
|
|
// separate protocol from the mutation receipt so verification is not authored
|
|
// by the executor result.
|
|
type DockerContainerObservationPayload struct {
|
|
RequestID string `json:"request_id"`
|
|
ActionID string `json:"action_id"`
|
|
ProtocolVersion int `json:"protocol_version"`
|
|
RequestDigest string `json:"request_digest"`
|
|
Runtime string `json:"runtime"`
|
|
ContainerID string `json:"container_id"`
|
|
}
|
|
|
|
type DockerContainerObservationResultPayload struct {
|
|
RequestID string `json:"request_id"`
|
|
ActionID string `json:"action_id"`
|
|
ProtocolVersion int `json:"protocol_version"`
|
|
RequestDigest string `json:"request_digest"`
|
|
Observed bool `json:"observed"`
|
|
ReasonCode string `json:"reason_code,omitempty"`
|
|
Snapshot DockerContainerObservationSnapshot `json:"snapshot"`
|
|
}
|
|
|
|
type DockerContainerLifecycleResultPayload struct {
|
|
RequestID string `json:"request_id"`
|
|
ActionID string `json:"action_id"`
|
|
Operation string `json:"operation"`
|
|
OperationVersion int `json:"operation_version"`
|
|
RequestDigest string `json:"request_digest"`
|
|
ContainerID string `json:"container_id"`
|
|
ExecutionPhase string `json:"execution_phase"`
|
|
MutationStarted bool `json:"mutation_started"`
|
|
MutationCompleted bool `json:"mutation_completed"`
|
|
ReadbackRan bool `json:"readback_ran"`
|
|
Before DockerContainerLifecycleSnapshot `json:"before"`
|
|
After DockerContainerLifecycleSnapshot `json:"after"`
|
|
ReasonCode string `json:"reason_code,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
Duration int64 `json:"duration_ms"`
|
|
}
|
|
|
|
const (
|
|
DockerContainerPhasePreflight = "preflight"
|
|
DockerContainerPhaseMutate = "mutate"
|
|
DockerContainerPhaseVerify = "verify"
|
|
DockerContainerPhaseComplete = "complete"
|
|
)
|
|
|
|
// DockerContainerUpdatePayload is the closed, typed container image update
|
|
// operation. It carries an immutable container ID and the exact image digest
|
|
// the plan observed; the agent refuses when the container no longer runs that
|
|
// digest, then pulls the container's own configured image reference and
|
|
// recreates it from its inspected configuration. It never carries command
|
|
// text, flags, a pull reference, or a user-controlled container name.
|
|
type DockerContainerUpdatePayload struct {
|
|
RequestID string `json:"request_id"`
|
|
ActionID string `json:"action_id"`
|
|
Operation string `json:"operation"`
|
|
OperationVersion int `json:"operation_version"`
|
|
RequestDigest string `json:"request_digest"`
|
|
Runtime string `json:"runtime"`
|
|
ContainerID string `json:"container_id"`
|
|
ExpectedImageDigest string `json:"expected_image_digest"`
|
|
Timeout int `json:"timeout,omitempty"`
|
|
}
|
|
|
|
// DockerContainerUpdateResultPayload reports an update outcome. ContainerID
|
|
// is the container the request targeted; a successful update recreates it, so
|
|
// NewContainerID carries the replacement identity and the compensation fields
|
|
// say whether a backup existed and whether rollback was applied on failure.
|
|
type DockerContainerUpdateResultPayload struct {
|
|
RequestID string `json:"request_id"`
|
|
ActionID string `json:"action_id"`
|
|
Operation string `json:"operation"`
|
|
OperationVersion int `json:"operation_version"`
|
|
RequestDigest string `json:"request_digest"`
|
|
ContainerID string `json:"container_id"`
|
|
ExecutionPhase string `json:"execution_phase"`
|
|
MutationStarted bool `json:"mutation_started"`
|
|
MutationCompleted bool `json:"mutation_completed"`
|
|
ReadbackRan bool `json:"readback_ran"`
|
|
NewContainerID string `json:"new_container_id,omitempty"`
|
|
ContainerName string `json:"container_name,omitempty"`
|
|
OldImageDigest string `json:"old_image_digest,omitempty"`
|
|
NewImageDigest string `json:"new_image_digest,omitempty"`
|
|
BackupCreated bool `json:"backup_created"`
|
|
BackupContainer string `json:"backup_container,omitempty"`
|
|
RollbackAttempted bool `json:"rollback_attempted"`
|
|
RolledBack bool `json:"rolled_back"`
|
|
After DockerContainerLifecycleSnapshot `json:"after"`
|
|
ReasonCode string `json:"reason_code,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
Duration int64 `json:"duration_ms"`
|
|
}
|
|
|
|
// DockerContainerUpdateOutcome is the neutral bridge between the Docker
|
|
// module's update implementation and the typed command channel, so the host
|
|
// command client does not import the Docker module directly.
|
|
type DockerContainerUpdateOutcome struct {
|
|
Success bool
|
|
ContainerName string
|
|
OldContainerID string
|
|
NewContainerID string
|
|
OldImageDigest string
|
|
NewImageDigest string
|
|
BackupCreated bool
|
|
BackupContainer string
|
|
RollbackAttempted bool
|
|
RolledBack bool
|
|
Error string
|
|
}
|
|
|
|
// ProxmoxGuestLifecyclePayload is a closed guest lifecycle operation. GuestKind
|
|
// selects one fixed Proxmox tool, Operation selects one fixed verb, and VMID is
|
|
// numeric. No command text or caller-supplied argument vector crosses the wire.
|
|
type ProxmoxGuestLifecyclePayload struct {
|
|
RequestID string `json:"request_id"`
|
|
ActionID string `json:"action_id"`
|
|
Operation string `json:"operation"`
|
|
OperationVersion int `json:"operation_version"`
|
|
RequestDigest string `json:"request_digest"`
|
|
GuestKind string `json:"guest_kind"`
|
|
VMID int `json:"vmid"`
|
|
ExpectedStatus string `json:"expected_status"`
|
|
Timeout int `json:"timeout,omitempty"`
|
|
}
|
|
|
|
type ProxmoxGuestLifecycleSnapshot struct {
|
|
Status string `json:"status,omitempty"`
|
|
ObservedAt time.Time `json:"observed_at,omitempty"`
|
|
}
|
|
|
|
type ProxmoxGuestLifecycleResultPayload struct {
|
|
RequestID string `json:"request_id"`
|
|
ActionID string `json:"action_id"`
|
|
Operation string `json:"operation"`
|
|
OperationVersion int `json:"operation_version"`
|
|
RequestDigest string `json:"request_digest"`
|
|
GuestKind string `json:"guest_kind"`
|
|
VMID int `json:"vmid"`
|
|
ExecutionPhase string `json:"execution_phase"`
|
|
MutationStarted bool `json:"mutation_started"`
|
|
MutationCompleted bool `json:"mutation_completed"`
|
|
ReadbackRan bool `json:"readback_ran"`
|
|
Before ProxmoxGuestLifecycleSnapshot `json:"before"`
|
|
After ProxmoxGuestLifecycleSnapshot `json:"after"`
|
|
ReasonCode string `json:"reason_code,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
Duration int64 `json:"duration_ms"`
|
|
}
|
|
|
|
const (
|
|
ProxmoxGuestPhasePreflight = "preflight"
|
|
ProxmoxGuestPhaseMutate = "mutate"
|
|
ProxmoxGuestPhaseVerify = "verify"
|
|
ProxmoxGuestPhaseComplete = "complete"
|
|
)
|
|
|
|
// HostUpdatePayload is the closed, typed host-package operation sent to a
|
|
// Unified Agent. It intentionally has no command or package-name fields: the
|
|
// agent owns the package-manager command catalog and always updates the whole
|
|
// bounded set returned by its own preflight simulation.
|
|
type HostUpdatePayload struct {
|
|
RequestID string `json:"request_id"`
|
|
ActionID string `json:"action_id"`
|
|
Operation string `json:"operation"`
|
|
OperationVersion int `json:"operation_version"`
|
|
RequestDigest string `json:"request_digest"`
|
|
ExpectedInventoryHash string `json:"expected_inventory_hash"`
|
|
Timeout int `json:"timeout,omitempty"`
|
|
}
|
|
|
|
type HostPackageUpdate struct {
|
|
Name string `json:"name"`
|
|
InstalledVersion string `json:"installed_version,omitempty"`
|
|
AvailableVersion string `json:"available_version,omitempty"`
|
|
}
|
|
|
|
type HostPackageUpdateSnapshot struct {
|
|
Supported bool `json:"supported"`
|
|
Manager string `json:"manager,omitempty"`
|
|
InventoryHash string `json:"inventory_hash,omitempty"`
|
|
PendingCount int `json:"pending_count"`
|
|
Packages []HostPackageUpdate `json:"packages,omitempty"`
|
|
CheckedAt time.Time `json:"checked_at,omitempty"`
|
|
RebootRequired bool `json:"reboot_required,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
const (
|
|
HostUpdateVerificationVerified = "verified"
|
|
HostUpdateVerificationFailed = "failed"
|
|
HostUpdateVerificationInconclusive = "inconclusive"
|
|
)
|
|
|
|
// HostUpdateResultPayload carries execution and read-after-write evidence
|
|
// independently. Success means the package-manager mutation completed;
|
|
// Verification states whether the postcondition was actually observed.
|
|
type HostUpdateResultPayload struct {
|
|
RequestID string `json:"request_id"`
|
|
ActionID string `json:"action_id"`
|
|
Success bool `json:"success"`
|
|
ExecutionPhase string `json:"execution_phase"`
|
|
MutationStarted bool `json:"mutation_started"`
|
|
Before HostPackageUpdateSnapshot `json:"before"`
|
|
After HostPackageUpdateSnapshot `json:"after"`
|
|
HealthChecked bool `json:"health_checked"`
|
|
PackageManagerHealthy bool `json:"package_manager_healthy"`
|
|
RecoveryRequired bool `json:"recovery_required"`
|
|
Verification string `json:"verification"`
|
|
ReasonCode string `json:"reason_code,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
Duration int64 `json:"duration_ms"`
|
|
}
|
|
|
|
const (
|
|
HostUpdatePhasePreflight = "preflight"
|
|
HostUpdatePhaseRefresh = "refresh"
|
|
HostUpdatePhaseInstall = "install"
|
|
HostUpdatePhaseVerify = "verify"
|
|
HostUpdatePhaseComplete = "complete"
|
|
)
|
|
|
|
const HostStorageCleanupOperationPackageCache = "clean_package_cache"
|
|
|
|
const HostStorageCleanupMaxReportedBytes int64 = 1 << 60
|
|
|
|
// HostStorageCleanupPayload is the closed storage-pressure operation sent to
|
|
// a Unified Agent. The model cannot select paths or commands: the agent owns
|
|
// the single APT package-cache target and command catalog.
|
|
type HostStorageCleanupPayload struct {
|
|
RequestID string `json:"request_id"`
|
|
ActionID string `json:"action_id"`
|
|
Operation string `json:"operation"`
|
|
OperationVersion int `json:"operation_version"`
|
|
RequestDigest string `json:"request_digest"`
|
|
ExpectedFingerprint string `json:"expected_fingerprint"`
|
|
Timeout int `json:"timeout,omitempty"`
|
|
}
|
|
|
|
type HostStorageCleanupSnapshot struct {
|
|
Supported bool `json:"supported"`
|
|
Provider string `json:"provider,omitempty"`
|
|
Fingerprint string `json:"fingerprint,omitempty"`
|
|
ReclaimableBytes int64 `json:"reclaimable_bytes"`
|
|
CheckedAt time.Time `json:"checked_at,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
const (
|
|
HostStorageCleanupVerificationVerified = "verified"
|
|
HostStorageCleanupVerificationFailed = "failed"
|
|
HostStorageCleanupVerificationInconclusive = "inconclusive"
|
|
)
|
|
|
|
// HostStorageCleanupResultPayload separates mutation success from the
|
|
// read-after-write observation of reclaimed cache bytes.
|
|
type HostStorageCleanupResultPayload struct {
|
|
RequestID string `json:"request_id"`
|
|
ActionID string `json:"action_id"`
|
|
Success bool `json:"success"`
|
|
ExecutionPhase string `json:"execution_phase"`
|
|
MutationStarted bool `json:"mutation_started"`
|
|
Before HostStorageCleanupSnapshot `json:"before"`
|
|
After HostStorageCleanupSnapshot `json:"after"`
|
|
ReclaimedBytes int64 `json:"reclaimed_bytes"`
|
|
Verification string `json:"verification"`
|
|
ReasonCode string `json:"reason_code,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
Duration int64 `json:"duration_ms"`
|
|
}
|
|
|
|
// ActionPreflightPayload carries exactly one already-bound typed operation to
|
|
// the Unified Agent for a read-only feasibility decision. It grants no durable
|
|
// dispatch authority and must never enter the operation receipt store.
|
|
type ActionPreflightPayload struct {
|
|
RequestID string `json:"request_id"`
|
|
ProtocolVersion int `json:"protocol_version"`
|
|
HostUpdate *HostUpdatePayload `json:"host_update,omitempty"`
|
|
StorageCleanup *HostStorageCleanupPayload `json:"storage_cleanup,omitempty"`
|
|
DockerLifecycle *DockerContainerLifecyclePayload `json:"docker_lifecycle,omitempty"`
|
|
DockerUpdate *DockerContainerUpdatePayload `json:"docker_update,omitempty"`
|
|
}
|
|
|
|
// ActionPreflightResultPayload is bounded agent evidence about the exact
|
|
// operation digest. No provider output, paths, package names, or command text
|
|
// cross this boundary.
|
|
type ActionPreflightResultPayload struct {
|
|
RequestID string `json:"request_id"`
|
|
ProtocolVersion int `json:"protocol_version"`
|
|
Operation string `json:"operation"`
|
|
OperationVersion int `json:"operation_version"`
|
|
RequestDigest string `json:"request_digest"`
|
|
Feasible bool `json:"feasible"`
|
|
ReasonCode string `json:"reason_code,omitempty"`
|
|
CheckedAt time.Time `json:"checked_at"`
|
|
}
|
|
|
|
const (
|
|
HostStorageCleanupPhasePreflight = "preflight"
|
|
HostStorageCleanupPhaseClean = "clean"
|
|
HostStorageCleanupPhaseVerify = "verify"
|
|
HostStorageCleanupPhaseComplete = "complete"
|
|
)
|
|
|
|
// Stable pre-mutation refusal codes cross the Unified Agent boundary without
|
|
// exposing command output, package names, paths, or provider error text. The
|
|
// server uses these codes for operator presentation and aggregate telemetry;
|
|
// model reasoning continues to consume the typed capability catalog rather
|
|
// than a hard-coded diagnostic tree.
|
|
const (
|
|
ActionRefusalContractInvalid = "action_contract_invalid"
|
|
ActionRefusalCapabilityUnavailable = "agent_capability_unavailable"
|
|
ActionRefusalTargetInspectionUnavailable = "target_inspection_unavailable"
|
|
ActionRefusalTargetStateChanged = "target_state_changed"
|
|
ActionRefusalTargetPreconditionFailed = "target_precondition_failed"
|
|
ActionRefusalPackageManagerBusy = "package_manager_busy"
|
|
ActionRefusalPackageIndexRefreshFailed = "package_index_refresh_failed"
|
|
ActionRefusalPackagePreflightFailed = "package_preflight_failed"
|
|
ActionRefusalPackageInventoryChanged = "package_inventory_changed"
|
|
ActionRefusalPackageManagerUnhealthy = "package_manager_unhealthy"
|
|
ActionRefusalCleanupPreflightFailed = "cleanup_preflight_failed"
|
|
ActionRefusalCleanupInventoryChanged = "cleanup_inventory_changed"
|
|
)
|
|
|
|
// IsActionRefusalReasonCode validates the bounded machine-code shape accepted
|
|
// from an agent result. It intentionally validates shape rather than a closed
|
|
// enum so a newer agent can report a new safe category to an older server.
|
|
func IsActionRefusalReasonCode(code string) bool {
|
|
code = strings.TrimSpace(code)
|
|
if len(code) == 0 || len(code) > 64 {
|
|
return false
|
|
}
|
|
for _, r := range code {
|
|
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '_' {
|
|
continue
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// ConnectedAgent represents an agent connected via WebSocket
|
|
type ConnectedAgent struct {
|
|
OrganizationID string
|
|
TokenID string
|
|
AgentID string
|
|
Hostname string
|
|
Version string
|
|
Platform string
|
|
Tags []string
|
|
RuntimeRole string
|
|
ActionCapability string
|
|
ConnectedAt time.Time
|
|
OperationReceiptVersion int
|
|
ActionPreflightVersion int
|
|
DockerObservationVersion int
|
|
}
|
|
|
|
// --- Deploy protocol payloads ---
|
|
|
|
const MaxDeployParallel = 10
|
|
|
|
// NormalizeDeployMaxParallel applies the shared server/agent concurrency
|
|
// contract before the value reaches worker or channel allocation boundaries.
|
|
func NormalizeDeployMaxParallel(value, defaultValue int) int {
|
|
if value <= 0 {
|
|
value = defaultValue
|
|
}
|
|
if value > MaxDeployParallel {
|
|
return MaxDeployParallel
|
|
}
|
|
return value
|
|
}
|
|
|
|
// DeployPreflightPayload is sent by the server to request SSH preflight checks
|
|
// against cluster peer nodes.
|
|
type DeployPreflightPayload struct {
|
|
RequestID string `json:"request_id"`
|
|
JobID string `json:"job_id"`
|
|
Targets []DeployPreflightTarget `json:"targets"`
|
|
PulseURL string `json:"pulse_url"` // URL peers must reach
|
|
MaxParallel int `json:"max_parallel,omitempty"` // 0 = sequential
|
|
Timeout int `json:"timeout,omitempty"` // per-target seconds, 0 = 120
|
|
}
|
|
|
|
// DeployPreflightTarget identifies a single node to preflight-check via SSH.
|
|
type DeployPreflightTarget struct {
|
|
TargetID string `json:"target_id"` // deploy.Target.ID
|
|
NodeName string `json:"node_name"` // Proxmox node name
|
|
NodeIP string `json:"node_ip"` // IP to SSH into
|
|
}
|
|
|
|
// DeployInstallPayload is sent by the server to request agent installation
|
|
// on cluster peer nodes via SSH.
|
|
type DeployInstallPayload struct {
|
|
RequestID string `json:"request_id"`
|
|
JobID string `json:"job_id"`
|
|
Targets []DeployInstallTarget `json:"targets"`
|
|
PulseURL string `json:"pulse_url"`
|
|
MaxParallel int `json:"max_parallel,omitempty"`
|
|
Timeout int `json:"timeout,omitempty"` // per-target seconds, 0 = 300
|
|
}
|
|
|
|
// DeployInstallTarget identifies a single node for agent installation via SSH.
|
|
type DeployInstallTarget struct {
|
|
TargetID string `json:"target_id"` // deploy.Target.ID
|
|
NodeName string `json:"node_name"`
|
|
NodeIP string `json:"node_ip"`
|
|
Arch string `json:"arch"` // from preflight: amd64, arm64
|
|
BootstrapToken string `json:"bootstrap_token"` // per-target enrollment token
|
|
}
|
|
|
|
// DeployCancelPayload is sent by the server to cancel an in-flight deploy job.
|
|
type DeployCancelPayload struct {
|
|
RequestID string `json:"request_id"`
|
|
JobID string `json:"job_id"`
|
|
}
|
|
|
|
// DeployProgressPayload is sent by the agent to stream progress events for
|
|
// preflight checks and install operations. Multiple progress messages are
|
|
// sent per request, with the final message having Final=true.
|
|
type DeployProgressPayload struct {
|
|
RequestID string `json:"request_id"`
|
|
JobID string `json:"job_id"`
|
|
TargetID string `json:"target_id,omitempty"` // empty for job-level events
|
|
Phase DeployProgressPhase `json:"phase"`
|
|
Status DeployStepStatus `json:"status"`
|
|
Message string `json:"message"`
|
|
Data string `json:"data,omitempty"` // JSON blob for structured results
|
|
Final bool `json:"final"` // true = last message for this request
|
|
}
|
|
|
|
// DeployProgressPhase identifies which stage of the deploy lifecycle this
|
|
// progress event belongs to.
|
|
type DeployProgressPhase string
|
|
|
|
const (
|
|
DeployPhasePreflightSSH DeployProgressPhase = "preflight_ssh"
|
|
DeployPhasePreflightArch DeployProgressPhase = "preflight_arch"
|
|
DeployPhasePreflightReach DeployProgressPhase = "preflight_reachability"
|
|
DeployPhasePreflightAgent DeployProgressPhase = "preflight_existing_agent"
|
|
DeployPhasePreflightComplete DeployProgressPhase = "preflight_complete"
|
|
DeployPhaseInstallTransfer DeployProgressPhase = "install_transfer"
|
|
DeployPhaseInstallExecute DeployProgressPhase = "install_execute"
|
|
DeployPhaseInstallEnrollWait DeployProgressPhase = "install_enroll_wait"
|
|
DeployPhaseInstallComplete DeployProgressPhase = "install_complete"
|
|
DeployPhaseCanceled DeployProgressPhase = "canceled"
|
|
DeployPhaseJobComplete DeployProgressPhase = "job_complete"
|
|
)
|
|
|
|
// DeployStepStatus is the outcome of a single progress step.
|
|
type DeployStepStatus string
|
|
|
|
const (
|
|
DeployStepStarted DeployStepStatus = "started"
|
|
DeployStepOK DeployStepStatus = "ok"
|
|
DeployStepFailed DeployStepStatus = "failed"
|
|
DeployStepSkipped DeployStepStatus = "skipped"
|
|
)
|
|
|
|
// PreflightResultData is the structured data for a completed preflight check.
|
|
// Serialized as JSON in DeployProgressPayload.Data.
|
|
type PreflightResultData struct {
|
|
Arch string `json:"arch,omitempty"`
|
|
HasAgent bool `json:"has_agent"`
|
|
PulseReachable bool `json:"pulse_reachable"`
|
|
SSHReachable bool `json:"ssh_reachable"`
|
|
ErrorDetail string `json:"error_detail,omitempty"`
|
|
}
|
|
|
|
// InstallResultData is the structured data for a completed install step.
|
|
// Serialized as JSON in DeployProgressPayload.Data.
|
|
type InstallResultData struct {
|
|
ExitCode int `json:"exit_code"`
|
|
Output string `json:"output,omitempty"`
|
|
}
|