package release import ( "context" "crypto/ed25519" "github.com/carlosframework/platform/internal/registry" ) // StageHeld is a held row's staging step, ensureStaged, with nothing // published: the version lands in the version store and live/ is // left alone. It refuses while CARLOS_HOST_CLAIM is on, and a refused write // is ErrStageBlocked wrapping the cause. // // It stages only a build §4.1 authorizes (settings spec rev 11 §4.3a): // HeldAuthorized, under pub and the box's ratchet r, answers first, and the // staging binds the digest it returns. A build nothing authorizes is // ErrHeldNotAuthorized, and nothing is downloaded. func StageHeld(ctx context.Context, s Store, pub ed25519.PublicKey, r *DigestRatchet, optDir string, rt registry.Route, version string) (*Manifest, error) { if err := heldStageable(rt, version); err != nil { return nil, err } return stageHeld(ctx, s, pub, r, optDir, rt, version) } // PublishHeld points live/ at a staged version, and does it inside // guard: guard re-checks whatever authorised the stage (for the root relay, // the registry hold frozen and the host claim re-read) and calls publish // only if that still holds, as its last step. Staging can take minutes, so // an authority checked only before it could be stale by now: a move aborted // or replaced meanwhile must never have live/ repointed for it. A nil // guard is refused: nothing is published on a request's word alone. // // Before guard it runs D3 as adoption does (admit, then bind on the staged // bytes), so a release the box's proof policy does not admit is // ErrHeldNotProven and live/ is untouched. A nil proof is refused. func PublishHeld(ctx context.Context, s Store, optDir string, rt registry.Route, version string, proof *ProofGate, guard func(publish func() error) error) error { if err := heldStageable(rt, version); err != nil { return err } if guard == nil { return errHeldNoGuard } if proof == nil { return errHeldNoProofGate } proof.BeginPass() ptr, v, err := heldProofAdmit(ctx, s, optDir, rt, version, proof) if err != nil { return err } return publishHeld(optDir, rt, version, proof, ptr, v, guard) } // ServeHeldStageRequests is the root side of the held-stage relay, run by // `carlos adopt` after its release adoption. For each .json in // HeldStageDir it validates every field before any filesystem effect (Host // equal to name and a valid host shape, valid account, app and version, a // non-empty op and box), asks authorize, stages, asks authorize again with // the publication as its then, writes the HeldStaged ack and removes the // request. A request that fails validation, that authorize refuses at // either point with ErrStaleHeldStage, that D3 refuses (proof, as // MaterializeHeld applies it: ErrHeldNotProven), or that §4.1 does not // authorize (ErrHeldNotAuthorized, or a build that does not hash to its // authorizing digest: ErrManifestDigest) is removed with a log line, // and live/ is never repointed for it. A request whose staging, ack or // publication fails any other way is kept for the next pass. With no // request directory it reads nothing and creates nothing. // // authorize(req, then) checks req and, if it passes, runs then (when // non-nil) under whatever guard keeps the check true until then returns. // Before staging it is called with a nil then. // // The directory is agent-owned and this runs as root, so every read, write // and removal goes through safefile: no symlink is followed at any // component, and only a regular file with one link is read. func ServeHeldStageRequests(ctx context.Context, s Store, pub ed25519.PublicKey, r *DigestRatchet, optDir, dataDir string, proof *ProofGate, authorize func(req HeldStageRequest, then func() error) error) error { return ServeHeldStageRequestsWithPins(ctx, s, pub, r, optDir, dataDir, proof, authorize) }