Skip to content

Commit d99fd8a

Browse files
TeoSlayerteovlclaude
authored
feat(managed): add standalone hosted node adoption and control runtime (#450)
* web4: WIP enterprise-control daemon wiring + governed pilotctl + IPC unbind + test fixes Preservation snapshot of uncommitted working-tree work (repo survey 2026-08-02). WIP branch — do NOT push directly; split into reviewed PRs first. Build scratch excluded via .gitignore. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * sec(enterprise): require explicit distinct fleet state_directory (no config-dir default) L1: with fleet state sync enabled and state_directory empty, the scan root defaulted to '.', the directory holding enterprise-control.json — so scanFleetState shipped 256KB previews of any operator-added text file there to the authority as signed telemetry. Fail closed: require state_directory to be set explicitly and to be distinct from the config directory. SECURITY_REVIEW_v1.14 L1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * sec(enterprise): local idempotency for signed lifecycle commands (anti-replay) M2: a captured authority-signed restart/shutdown command could be replayed by a compromised or MITM'd authority connection every poll — and after the restart it caused — for up to its 24h TTL, because the daemon's result report goes to the attacker (who drops it) so the authority's server-side de-duplication never engages, yielding a fleet-wide shutdown/restart boot-loop. The signature is replayed, not forged. Persist a lifecycle guard (monotonic IssuedAt high-water + last command id) to the enterprise-control state dir and refuse any lifecycle command at or below it. The record is written BEFORE the daemon acts (and the write failing is fatal to the action — fail closed), so it survives the syscall.Exec restart and the replay is rejected on the next poll. Regression test covers replay, older issue-time, newer command, and restart survival. (Cert-pinning the authority channel — the other half of the MITM precondition — remains a tracked enhancement; this idempotency record already breaks the replay loop.) SECURITY_REVIEW_v1.14 M2 (idempotency). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(enterprise): add hosted action control and node adoption * fix(enterprise): install policy from enrollment claim * fix(enterprise): acknowledge active policy enforcement * fix(enterprise): separate core adoption from MCP * fix(managed): decouple runtime from private platform sources * refactor(managed): keep hosted fleet services private * docs(cli): refresh managed command reference * security(managed): document bounded local operations --------- Co-authored-by: Teodor Calin <teodor@vulturelabs.io> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent d7a840c commit d99fd8a

69 files changed

Lines changed: 18202 additions & 119 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cmd/control-agent/main.go

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
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

Comments
 (0)