package planner import ( "crypto/sha256" "encoding/hex" "encoding/json" "errors" "time" ) const planLifetime = 15 * time.Minute // Plan is an offline intent preview, not an executable authorization. Hash binds // its contents but is not a signature. No secret may be added to this structure. type Plan struct { Intent Intent `json:"intent"` ProjectName string `json:"projectName"` DataPath string `json:"dataPath"` CreatedAt time.Time `json:"createdAt"` ExpiresAt time.Time `json:"expiresAt"` Hash string `json:"hash"` } func Build(i Intent, now time.Time) (Plan, error) { if err := i.Validate(); err != nil { return Plan{}, err } now = now.UTC().Truncate(time.Second) p := Plan{Intent: i, ProjectName: "sd-" + i.InstanceID, DataPath: "/var/lib/server-deploy/instances/" + i.InstanceID, CreatedAt: now, ExpiresAt: now.Add(planLifetime)} encoded, err := json.Marshal(p) if err != nil { return Plan{}, errors.New("cannot encode plan") } sum := sha256.Sum256(encoded) p.Hash = "sha256:" + hex.EncodeToString(sum[:]) return p, nil } // Verify checks offline consistency only. Current must be independently inspected // under a host lock before any future write operation uses this comparison. func (p Plan) Verify(current Intent, now time.Time) error { if now.Before(p.CreatedAt) || !now.Before(p.ExpiresAt) { return errors.New("plan is not within its validity window") } if p.Intent != current { return errors.New("plan does not match current intent or observed state") } expected, err := Build(current, p.CreatedAt) if err != nil { return err } if p.ProjectName != expected.ProjectName || p.DataPath != expected.DataPath || !p.CreatedAt.Equal(expected.CreatedAt) || !p.ExpiresAt.Equal(expected.ExpiresAt) || p.Hash != expected.Hash { return errors.New("plan integrity check failed") } return nil }