|
| 1 | +// SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | + |
| 3 | +// Command control-agent is a headless reference node for Pilot's optional |
| 4 | +// hosted control plane. It runs the same enterprisecontrol runtime as the Web4 |
| 5 | +// daemon without opening a Pilot transport, making signed fleet operations |
| 6 | +// usable beside any agent harness. It intentionally exposes no remote shell. |
| 7 | +package main |
| 8 | + |
| 9 | +import ( |
| 10 | + "context" |
| 11 | + "crypto/sha256" |
| 12 | + "encoding/hex" |
| 13 | + "encoding/json" |
| 14 | + "errors" |
| 15 | + "flag" |
| 16 | + "fmt" |
| 17 | + "os" |
| 18 | + "os/signal" |
| 19 | + "path/filepath" |
| 20 | + "syscall" |
| 21 | + "time" |
| 22 | + |
| 23 | + "github.com/pilot-protocol/common/actionhook" |
| 24 | + "github.com/pilot-protocol/pilotprotocol/internal/enterprisecontrol" |
| 25 | + "github.com/pilot-protocol/pilotprotocol/internal/managedsdk/authority" |
| 26 | +) |
| 27 | + |
| 28 | +type evidenceEvent struct { |
| 29 | + Event string `json:"event"` |
| 30 | + CommandID string `json:"command_id,omitempty"` |
| 31 | + Detail string `json:"detail,omitempty"` |
| 32 | + PID int `json:"pid"` |
| 33 | + RuntimeVersion string `json:"runtime_version"` |
| 34 | + ObservedAt int64 `json:"observed_at"` |
| 35 | +} |
| 36 | + |
| 37 | +type diagnosticRecord struct { |
| 38 | + Version uint16 `json:"version"` |
| 39 | + CommandID string `json:"command_id"` |
| 40 | + Hostname string `json:"hostname"` |
| 41 | + PID int `json:"pid"` |
| 42 | + UID int `json:"uid"` |
| 43 | + RuntimeVersion string `json:"runtime_version"` |
| 44 | + PolicyRevision uint64 `json:"policy_revision"` |
| 45 | + StartedAt int64 `json:"started_at"` |
| 46 | + ObservedAt int64 `json:"observed_at"` |
| 47 | +} |
| 48 | + |
| 49 | +func main() { |
| 50 | + controlPath := flag.String("enterprise-control", "", "path to the signed enterprise control attachment") |
| 51 | + runtimeVersion := flag.String("runtime-version", "pilot-control-agent/1.0.0", "reported runtime version") |
| 52 | + nodeID := flag.Uint("node-id", 1001, "reported Pilot node ID") |
| 53 | + poll := flag.Duration("poll-interval", 2*time.Second, "fleet control poll interval") |
| 54 | + evidenceDirectory := flag.String("evidence-dir", "", "owner-only directory for tangible lifecycle and diagnostic evidence") |
| 55 | + flag.Parse() |
| 56 | + if *controlPath == "" || *evidenceDirectory == "" || *poll < 250*time.Millisecond || *poll > time.Minute || *nodeID == 0 || *nodeID > uint(^uint32(0)) { |
| 57 | + fatalf("enterprise-control, evidence-dir, a node ID, and a 250ms-1m poll interval are required") |
| 58 | + } |
| 59 | + if err := os.MkdirAll(*evidenceDirectory, 0o700); err != nil { |
| 60 | + fatalf("create evidence directory: %v", err) |
| 61 | + } |
| 62 | + controls, err := enterprisecontrol.Load(*controlPath) |
| 63 | + if err != nil { |
| 64 | + fatalf("load enterprise controls: %v", err) |
| 65 | + } |
| 66 | + started := time.Now().UTC() |
| 67 | + if err := appendEvidence(*evidenceDirectory, evidenceEvent{Event: "startup", PID: os.Getpid(), RuntimeVersion: *runtimeVersion, ObservedAt: started.Unix()}); err != nil { |
| 68 | + fatalf("record startup: %v", err) |
| 69 | + } |
| 70 | + if err := runTangibleHook(context.Background(), controls, *evidenceDirectory); err != nil { |
| 71 | + fatalf("run tangible action hook: %v", err) |
| 72 | + } |
| 73 | + |
| 74 | + ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) |
| 75 | + defer cancel() |
| 76 | + ticker := time.NewTicker(*poll) |
| 77 | + defer ticker.Stop() |
| 78 | + for { |
| 79 | + // #nosec G115 -- nodeID is rejected above unless it fits exactly in uint32. |
| 80 | + lifecycle, err := synchronize(ctx, controls, uint32(*nodeID), *runtimeVersion, started, *evidenceDirectory) |
| 81 | + if err != nil && ctx.Err() == nil { |
| 82 | + _, _ = fmt.Fprintf(os.Stderr, "pilot-control-agent: synchronize: %v\n", err) |
| 83 | + } |
| 84 | + switch lifecycle { |
| 85 | + case "restart": |
| 86 | + if err := appendEvidence(*evidenceDirectory, evidenceEvent{Event: "restart_requested", PID: os.Getpid(), RuntimeVersion: *runtimeVersion, ObservedAt: time.Now().UTC().Unix()}); err != nil { |
| 87 | + fatalf("record restart: %v", err) |
| 88 | + } |
| 89 | + executable, err := os.Executable() |
| 90 | + if err != nil { |
| 91 | + fatalf("resolve executable: %v", err) |
| 92 | + } |
| 93 | + // #nosec G204,G702 -- restart re-execs the current OS-resolved binary directly; no shell or authority-supplied command is involved. |
| 94 | + if err := syscall.Exec(executable, os.Args, os.Environ()); err != nil { |
| 95 | + fatalf("restart process: %v", err) |
| 96 | + } |
| 97 | + case "shutdown": |
| 98 | + if err := appendEvidence(*evidenceDirectory, evidenceEvent{Event: "shutdown_requested", PID: os.Getpid(), RuntimeVersion: *runtimeVersion, ObservedAt: time.Now().UTC().Unix()}); err != nil { |
| 99 | + fatalf("record shutdown: %v", err) |
| 100 | + } |
| 101 | + return |
| 102 | + } |
| 103 | + select { |
| 104 | + case <-ctx.Done(): |
| 105 | + _ = appendEvidence(*evidenceDirectory, evidenceEvent{Event: "signal_shutdown", PID: os.Getpid(), RuntimeVersion: *runtimeVersion, ObservedAt: time.Now().UTC().Unix()}) |
| 106 | + return |
| 107 | + case <-ticker.C: |
| 108 | + } |
| 109 | + } |
| 110 | +} |
| 111 | + |
| 112 | +func synchronize(ctx context.Context, controls *enterprisecontrol.Runtime, nodeID uint32, runtimeVersion string, started time.Time, evidenceDirectory string) (string, error) { |
| 113 | + reconciliation, err := controls.ReconcileFleetControl(ctx, runtimeVersion) |
| 114 | + if err != nil { |
| 115 | + return "", err |
| 116 | + } |
| 117 | + if reconciliation.Found { |
| 118 | + if err := controls.ReportFleetControlAcknowledgement(ctx, reconciliation, runtimeVersion); err != nil { |
| 119 | + return "", err |
| 120 | + } |
| 121 | + } |
| 122 | + status := enterprisecontrol.FleetNodeStatus{ |
| 123 | + NodeID: nodeID, AgentVersion: runtimeVersion, UptimeSeconds: uint64(time.Since(started).Seconds()), |
| 124 | + PolicyRevision: controls.CurrentPolicyRevision(ctx), |
| 125 | + } |
| 126 | + if err := controls.ReportFleetStatus(ctx, status); err != nil { |
| 127 | + return "", err |
| 128 | + } |
| 129 | + commands, err := controls.FleetCommands(ctx) |
| 130 | + if err != nil { |
| 131 | + return "", err |
| 132 | + } |
| 133 | + for _, command := range commands { |
| 134 | + outcome, detail, lifecycle := executeCommand(ctx, controls, command, runtimeVersion, started, evidenceDirectory) |
| 135 | + if err := controls.ReportFleetCommandResult(ctx, command.ID, outcome, detail); err != nil { |
| 136 | + return "", err |
| 137 | + } |
| 138 | + if err := appendEvidence(evidenceDirectory, evidenceEvent{Event: "command_result", CommandID: command.ID, Detail: string(command.Kind) + ":" + outcome + ":" + detail, PID: os.Getpid(), RuntimeVersion: runtimeVersion, ObservedAt: time.Now().UTC().Unix()}); err != nil { |
| 139 | + return "", err |
| 140 | + } |
| 141 | + if outcome == "succeeded" && lifecycle != "" { |
| 142 | + if err := controls.MarkLifecycleCommandApplied(command); err != nil { |
| 143 | + return "", err |
| 144 | + } |
| 145 | + return lifecycle, nil |
| 146 | + } |
| 147 | + } |
| 148 | + return "", nil |
| 149 | +} |
| 150 | + |
| 151 | +func executeCommand(ctx context.Context, controls *enterprisecontrol.Runtime, command authority.FleetCommand, runtimeVersion string, started time.Time, evidenceDirectory string) (string, string, string) { |
| 152 | + switch command.Kind { |
| 153 | + case authority.FleetCommandRefreshPolicy: |
| 154 | + if err := controls.RefreshRollout(ctx); err != nil { |
| 155 | + return "failed", "rollout_refresh_failed", "" |
| 156 | + } |
| 157 | + return "succeeded", "policy_refreshed", "" |
| 158 | + case authority.FleetCommandExportReceipts: |
| 159 | + if !controls.HasReceiptExport() { |
| 160 | + return "rejected", "receipt_export_unconfigured", "" |
| 161 | + } |
| 162 | + if err := controls.ExportReceiptsOnce(ctx); err != nil { |
| 163 | + return "failed", "receipt_export_failed", "" |
| 164 | + } |
| 165 | + return "succeeded", "receipts_exported", "" |
| 166 | + case authority.FleetCommandReloadControl: |
| 167 | + if err := controls.Reload(); err != nil { |
| 168 | + return "failed", "control_reload_failed", "" |
| 169 | + } |
| 170 | + return "succeeded", "control_reloaded", "" |
| 171 | + case authority.FleetCommandSyncState: |
| 172 | + if !controls.HasFleetStateSync() { |
| 173 | + return "rejected", "state_sync_unconfigured", "" |
| 174 | + } |
| 175 | + if _, err := controls.SyncFleetState(ctx); err != nil { |
| 176 | + return "failed", "state_sync_failed", "" |
| 177 | + } |
| 178 | + return "succeeded", "state_synchronized", "" |
| 179 | + case authority.FleetCommandDiagnostics: |
| 180 | + if controls.HasFleetStateSync() { |
| 181 | + if _, err := controls.SyncFleetState(ctx); err != nil { |
| 182 | + return "failed", "diagnostics_sync_failed", "" |
| 183 | + } |
| 184 | + } |
| 185 | + if err := writeDiagnostics(evidenceDirectory, command.ID, runtimeVersion, controls.CurrentPolicyRevision(ctx), started); err != nil { |
| 186 | + return "failed", "diagnostics_write_failed", "" |
| 187 | + } |
| 188 | + return "succeeded", "diagnostics_written", "" |
| 189 | + case authority.FleetCommandRestartRuntime: |
| 190 | + if controls.LifecycleCommandAlreadyApplied(command) { |
| 191 | + return "rejected", "already_applied", "" |
| 192 | + } |
| 193 | + return "succeeded", "restart_accepted", "restart" |
| 194 | + case authority.FleetCommandShutdownRuntime: |
| 195 | + if controls.LifecycleCommandAlreadyApplied(command) { |
| 196 | + return "rejected", "already_applied", "" |
| 197 | + } |
| 198 | + return "succeeded", "shutdown_accepted", "shutdown" |
| 199 | + default: |
| 200 | + return "rejected", "command_not_allowlisted", "" |
| 201 | + } |
| 202 | +} |
| 203 | + |
| 204 | +func runTangibleHook(ctx context.Context, controls *enterprisecontrol.Runtime, evidenceDirectory string) error { |
| 205 | + hook := controls.ActionHook() |
| 206 | + if hook == nil { |
| 207 | + return fmt.Errorf("managed action hook is not configured") |
| 208 | + } |
| 209 | + target := filepath.Join(evidenceDirectory, "hook-side-effect.txt") |
| 210 | + if _, err := os.Stat(target); err == nil { |
| 211 | + return nil |
| 212 | + } else if !errors.Is(err, os.ErrNotExist) { |
| 213 | + return err |
| 214 | + } |
| 215 | + content := []byte("Pilot managed action hook released this tangible file write.\n") |
| 216 | + digest := sha256.Sum256(content) |
| 217 | + envelope, err := actionhook.NewEnvelope("file.write", "workspace:control-agent/hook-side-effect.txt", hex.EncodeToString(digest[:]), "pilot.control-agent", map[string]string{"content_type": "text/plain"}, time.Now().UTC()) |
| 218 | + if err != nil { |
| 219 | + return err |
| 220 | + } |
| 221 | + preflight, err := hook.BeforeAction(ctx, envelope) |
| 222 | + if err != nil { |
| 223 | + return err |
| 224 | + } |
| 225 | + if err := preflight.RequireUnconstrained(); err != nil { |
| 226 | + return err |
| 227 | + } |
| 228 | + if err := os.WriteFile(target, content, 0o600); err != nil { |
| 229 | + return err |
| 230 | + } |
| 231 | + if err := hook.AfterAction(ctx, envelope, preflight, actionhook.ObservedResult{Status: actionhook.StatusSucceeded, ObservedAt: time.Now().UTC().Unix()}); err != nil { |
| 232 | + return err |
| 233 | + } |
| 234 | + return appendEvidence(evidenceDirectory, evidenceEvent{Event: "managed_hook_side_effect", Detail: "file.write:allow", PID: os.Getpid(), RuntimeVersion: "pilot-control-agent/1.0.0", ObservedAt: time.Now().UTC().Unix()}) |
| 235 | +} |
| 236 | + |
| 237 | +func writeDiagnostics(directory, commandID, runtimeVersion string, policyRevision uint64, started time.Time) error { |
| 238 | + hostname, err := os.Hostname() |
| 239 | + if err != nil { |
| 240 | + return err |
| 241 | + } |
| 242 | + record := diagnosticRecord{Version: 1, CommandID: commandID, Hostname: hostname, PID: os.Getpid(), UID: os.Getuid(), RuntimeVersion: runtimeVersion, PolicyRevision: policyRevision, StartedAt: started.Unix(), ObservedAt: time.Now().UTC().Unix()} |
| 243 | + return writeSecureJSON(filepath.Join(directory, "diagnostics-"+commandID+".json"), record) |
| 244 | +} |
| 245 | + |
| 246 | +func appendEvidence(directory string, event evidenceEvent) error { |
| 247 | + path := filepath.Join(directory, "control-events.jsonl") |
| 248 | + // #nosec G304 -- path is a fixed filename beneath the owner-only evidence directory established at startup. |
| 249 | + file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600) |
| 250 | + if err != nil { |
| 251 | + return err |
| 252 | + } |
| 253 | + encodeErr := json.NewEncoder(file).Encode(event) |
| 254 | + closeErr := file.Close() |
| 255 | + return errors.Join(encodeErr, closeErr) |
| 256 | +} |
| 257 | + |
| 258 | +func writeSecureJSON(path string, value any) error { |
| 259 | + encoded, err := json.MarshalIndent(value, "", " ") |
| 260 | + if err != nil { |
| 261 | + return err |
| 262 | + } |
| 263 | + encoded = append(encoded, '\n') |
| 264 | + if err := os.WriteFile(path, encoded, 0o600); err != nil { |
| 265 | + return err |
| 266 | + } |
| 267 | + return os.Chmod(path, 0o600) |
| 268 | +} |
| 269 | + |
| 270 | +func fatalf(format string, arguments ...any) { |
| 271 | + _, _ = fmt.Fprintf(os.Stderr, "pilot-control-agent: "+format+"\n", arguments...) |
| 272 | + os.Exit(1) |
| 273 | +} |
0 commit comments