| 1 | # Signed runtime-contract bindings |
| 2 | |
| 3 | Proposed implementation supplement to approved maintenance-exec.md6a81dac. The required runtime-contract floor must be backed by an actual signed declaration. This supplement revises rejected 0e768aa against main11a and is pending independent review; no contract field or admission path is mounted yet. |
| 4 | |
| 5 | ## Existing source and the gap |
| 6 | |
| 7 | Source baseline is platform11a303996d5503d29ede77598243ddc66f735279. At this revision, release.Manifest is ordinary JSON. release.ChannelPointer is signed and names only app/channel/version. GetManifest validates JSON identity and artifact hashes but does not verify a manifest signature. putImmutable explicitly allows a pruned version name to be reused with different bytes. ensureStaged can return a local parsed sibling without obtaining the current pointer, re-marshals fetched manifests, and treats some missing-sibling errors as best-effort backfill. Those are existing deployment semantics; none proves a new authorization floor's declaration is cryptographically bound to the selected artifact. |
| 8 | |
| 9 | Adding RuntimeContracts alone would not satisfy the approved design. The new managed admission path must require a signed manifest binding and verify the actual staged binary. Existing unguarded legacy releases keep their current behavior. |
| 10 | |
| 11 | ## Additive wire fields |
| 12 | |
| 13 | Manifest gains runtime_contracts,omitempty: a sorted unique set of at most16 canonical names, using the existing internal/maintenanceguard/types.go ValidContracts: each name is 1–64 ASCII bytes matching ^[a-z][a-z0-9]*([.-][a-z0-9]+)*$, sorted strictly ascending, at most 16. Names are opaque declarations, not a platform registry: a syntactically valid unfamiliar name is preserved and compares by exact equality, never implies a known contract. Absent or an empty array means no declaration; explicit null, wrong types, duplicates, unsorted or invalid names refuse. Writers omit an empty set, preserving old emitted bytes. Contract-bearing manifests are binary releases; validateManifest rejects a nonbinary kind with a nonempty declaration before ship or signing, and the verified-result constructor repeats this check. Declarations express the trusted promoter's approval of exact release metadata at signing time, not immutable build provenance or proof that the implementation meets the contract. If metadata was changed before promotion, the signature authenticates what the promoter then approved. Review and actual compatibility tests remain separate. The meaning and approval of oficina.docs.team-authorization.v1 belongs to its separately reviewed Docs consumer contract and trusted promoter. Platform authenticates the declaration and compares the required set; it does not certify Docs behavior, infer review completion or grant consumer authority. |
| 14 | |
| 15 | ChannelPointer gains runtime_manifest,omitempty: an optional object with account and sha256. Account is the exact validated account containing the release. SHA256 is lowercase64-character hash of the exact manifest bytes read and validated by the signer. The pointer already binds app/channel/version and its signing grant; together these bind account, release identity, declared contracts and every artifact hash. The runtime_manifest object has exactly account and sha256, both required and nonempty. Explicit null, unknown members, duplicate members or case aliases, wrong types, incomplete objects and noncanonical digests refuse; only absent preserves legacy pointer parsing and bytes. Strict readers reject duplicate/case-aliased occurrences of either new outer field rather than accepting last-wins JSON. The signing payload includes the new object; old software ignoring it is excluded by the already required maintenance-aware writer/serving floor. |
| 16 | |
| 17 | A manifest bearing nonempty runtime_contracts requires this binding before it can supply compatibility evidence. Unsigned old pointers, plain staged JSON, filenames or a successful JSON parse never establish a required contract. A binding referring to a manifest with no contracts may be valid cryptographic evidence but cannot satisfy a nonempty required set. |
| 18 | |
| 19 | ## Signer construction and all pointer writers |
| 20 | |
| 21 | Promote and PromoteOnLadder obtain the target manifest's exact raw bytes and validated value in one read. For a contract-bearing target they set runtime_manifest to the target account and SHA256 of those same bytes before signing. Every newly selected target recomputes the binding; it never inherits the previous version's binding through a struct copy or history. A failed fetch/validation produces no pointer mutation. Contract-bearing raw manifests are bounded at1MiB; exceeding that fails before signing. Existing metadata validation still applies. |
| 22 | |
| 23 | All signPointer callers must be inventoried, including migrations and direct tests. A caller creating a new contracted target must use the checked target construction helper. A legacy migration may preserve an already verified unchanged pointer payload, but cannot attach a new binding without verifying the exact target bytes. Re-signing a pointer with a copied binding while changing its account/app/version is forbidden. Version-only history stays additive and unchanged. Rollback policy is specified below; the existing legacy pruned-target behavior is deliberately retained only outside contract-enabled selection. |
| 24 | |
| 25 | This extends the existing root/scoped signer model, without another private key or signature format. verifyPointer still enforces the embedded grant's original channel/time and root-only fast-track rules. New verification additionally checks runtime_manifest.account against the expected account. A valid declaration from another account cannot satisfy the floor. This does not retroactively sign old manifests or infer author intent from a matching version name. |
| 26 | |
| 27 | ## Verification and staged evidence |
| 28 | |
| 29 | Add a release-internal verified result with unexported construction state. It is produced only from a verified signed pointer wrapper plus exact manifest bytes: verify signature/grant/app/channel, account binding, manifest byte digest, manifest app/version/schema and declaration grammar, then artifact identity. Accessors return copies. The maintenance worker supplies the selected artifact bytes/digest and architecture; the checked result must prove they match the manifest. Required-contract comparison occurs only after these checks. |
| 30 | |
| 31 | Stage the exact manifest bytes and original verified signed pointer wrapper together as a versioned evidence file beside the binary, via the existing atomic-file helper. The file contains bounded raw-byte encodings, not re-marshaled manifest JSON. It is nonsecret and root-writable like the current staged manifest. Decode strictly bounds the raw signed wrapper at256KiB, raw manifest at1MiB and complete encoded evidence at2MiB before allocations; require exact canonical base64 encodings and closed evidence schema. A new contracted promotion that cannot fit these limits refuses before writing; legacy unguarded pointers retain their existing size behavior. |
| 32 | |
| 33 | On managed wake/adoption/resume, reverify staged evidence against the current trusted release root and expected account/app/version. Verify a retained wrapper using its own signed channel and signed-time grant rules, not the route's current channel. Separately verify current deployment selection under the actual target channel policy. Open the selected artifact as a regular nonsymlink read-only file, hash its actual bytes and retain the descriptor. The checked result owns both an immutable verified manifest copy and that open artifact; raw JSON or a pathname cannot construct it. Accessors return copies. Every managed consumer must accept this opaque handle, and capabilityPlan must obtain its capability set from this verified manifest rather than ReadStagedManifest's lossy mutable sibling. If a sibling disagrees it is not used; managed staging reports/refuses the conflict until supported restaging restores consistency. |
| 34 | |
| 35 | A different binary, modified declaration, unknown signing key/grant, changed account, absent evidence, missing required declaration, malformed or truncated evidence refuses before any managed process start or capability mutation. The outside-app guard's required set is supplied by the approved lifecycle integration, not inferred from an editable manifest. A nonempty binding/declaration also selects strict verification even where the required set is empty, so removing a floor alone cannot turn contract-bearing selection into legacy fallback. No nil-manifest leave-alone or best-effort backfill is managed success. |
| 36 | |
| 37 | The backend must obtain an explicit admission classification before Seed, Start or StartSidecar, rather than infer it from registry.Route alone. First consult the authoritative guard decision for the primary host; a read/validation error refuses, and Managed selects strict verification. Otherwise, any present staged evidence selects strict verification (malformed evidence refuses). Otherwise inspect the staged sibling: a declaration selects strict verification; malformed or unreadable present metadata refuses. Only an affirmatively unguarded route with neither evidence nor declared contracts can use legacy execution. A caller cannot inject this classification through route fields or CLI options. The selected opaque handle carries the classification and immutable verified release data through the backend seam. The guard-wiring implementation remains a prerequisite, not a claim it exists today. |
| 38 | |
| 39 | For managed or contract-bearing selection, ensureStaged's binary-present/evidence-absent branch must fetch the selected signed pointer and exact manifest, verify them and the existing artifact, and atomically publish matching evidence. It must not return a parsed sibling as success. If no current pointer can prove the required exact selected version, it refuses pending an authorized selection/restaging; it cannot invent historical evidence. This recovery is required after an artifact/evidence publication crash. A signed bound pointer or fetched declaration also selects strict staging before any legacy early return. |
| 40 | |
| 41 | The same verified manifest source is mandatory in internal/host/caps_pass.go stagedManifestOfLiveVersion/buildPlans and internal/host/memtier.go memTier, not only release.capabilityPlan. The hourly managed caps pass verifies the live selected version's evidence and derives capability changes exclusively from that copy; disagreement or unavailable proof yields no plan and no capability or DB-path mutation for the route. It does not authorize continued serving. Managed spawn derives the cgroup tier from its verified handle; it must not call the current no-error memTier callback, which silently defaults after a read failure. Verification failure refuses the spawn. Legacy readers retain their current leave-alone/default behavior. Tests mutate the sibling after verified adoption and prove neither the hourly pass nor subsequent serving/seed/sidecar spawn consumes the changed capabilities or tier. |
| 42 | |
| 43 | ## Normal execution handoff |
| 44 | |
| 45 | This is a new implementation contract, not an existing feature or the maintenance launch fd3/fd4 ABI. At the pinned baseline, ExecBackend.Seed, Start and StartSidecar in internal/activator/backend_exec.go each construct a command from a pathname inside spawnInCgroup. Start reads LiveVersion before spawning; that is not an artifact identity guarantee. All three managed paths must instead receive the opaque verified release/artifact handle above and construct every command from it. Preserve each path's existing argv, environment filtering, logging and cancellation policy; derive reported version from the verified handle, not a later symlink read. |
| 46 | |
| 47 | For native Linux managed binary execution, reserve child fd3 solely as the read-only verified executable: ExtraFiles contains exactly that file, and exec path is /proc/self/fd/3. There is no launch-context pipe or maintenance fd4. The app selects normal versus maintenance startup by the explicit argv/mode contract; it never probes fd3 to choose an authority path. The parent retains its handle until the child has completed; the child may inherit fd3 for its lifetime. Seed, serving and sidecar each acquire a separate verified handle and cannot share an ownership-close race. The same descriptor supplies the hash and kernel execution. Reject an unexpected ExtraFiles occupant, unsupported OS/backend, unavailable descriptor execution or any failure to retain the descriptor; never retry by pathname. No shell/script or nonbinary release is supported by this path. A pathname rename/replacement must execute the verified original inode or refuse. This assumes platform-controlled artifacts are not modified in place by trusted same-UID writers during execution; it is not isolation against malicious privileged inode modification. |
| 48 | |
| 49 | Normal serving retains its existing cgroup policy; this design does not reuse maintenance PrepareStrict, whose exclusive host reservation cannot accommodate a concurrent serving child and sidecar. The existing spawnInCgroup build callback may run twice. Every invocation must build a fresh command using the same retained verified artifact descriptor, the same verified version and copied argv/environment; the callback must not re-resolve a pathname or transfer/close the handle. A cgroup fallback may change placement under the existing normal policy but cannot change executable identity or use a pathname. Test the clone3 fallback branch with a replaced staged path. Maintenance retains its separate strict no-fallback requirement. Ordinary unguarded, unbound legacy releases keep their current spawn behavior. UnitBackend/systemd and other execution paths that cannot carry this verified descriptor refuse contract-enabled releases until a separately reviewed equivalent exists. Adoption must reject such a target before capability changes, symlink publication or restart. Maintenance execute/resume uses its separately reviewed worker launch adapter, consuming the same opaque verified handle but applying its distinct fd layout; this paragraph does not mount that adapter. |
| 50 | |
| 51 | |
| 52 | The original pointer wrapper is evidence of the release author's signed declaration, not proof that its channel is still current. The existing route/deployment selection determines which version may be chosen; the guard separately enforces compatibility. Keeping verified historical evidence permits a later ordinary wake after the channel has advanced, and supports explicitly selected compatible rollbacks. It must not be used to select a deployment, bypass a channel policy or manufacture a current owner approval. Scoped-grant validity is checked under the same signed-time rules as the existing pointer verifier. |
| 53 | |
| 54 | A crash between binary staging and evidence publication leaves managed admission refused. Recovery may fetch and verify matching evidence through the supported release path; it cannot synthesize it from a local manifest. Publication cannot overwrite a different same-version manifest/artifact pair and declare success. Evidence wrappers may differ across legitimate ladder promotions: for the same verified account/app/version and exact manifest/artifact bytes, keep an already valid wrapper or atomically replace it with another independently verified wrapper under the current trusted root. Never reject an edge-to-stable promotion merely because its signed wrapper differs, and never use the retained edge wrapper as stable selection authority. The current target channel policy is separately verified; this evidence proves compatibility only. Different manifest or artifact bytes remain a conflict and require explicit supported restaging rather than silent replacement. Reused pruned names with new raw manifests require their own signed binding and artifact verification; old evidence cannot authenticate new bytes. Required contracts remain in the outside-app guard across pruning/purge and cannot be removed by editing a manifest or ordinary config. |
| 55 | |
| 56 | ## Writer edge cases and legacy behavior |
| 57 | |
| 58 | At the pinned source, production signPointer calls are in release.go promote, Signer.Rollback and FastTrack; PromoteOnLadder delegates promotion after its same-head check. accounts.go runAccountsMigrate/isChannelPointer/verifyMigratedPointers copies signed wrappers and remains in the parser/copy inventory even though it does not call signPointer. FastTrack must preserve the unchanged account/app/version binding only after strict signature/account validation; it must never drop or replace it. If it creates a binding for previously unbound declared metadata, it uses the same checked target helper as promotion. Its existing already-waived no-op remains a no-op, not a binding repair mechanism. |
| 59 | |
| 60 | PromoteOnLadder's same-head shortcut stays unchanged for legacy metadata. For a contract-bearing target with an absent binding, an explicit authorized promotion to that same head must perform a CAS binding upgrade after fetching and validating exact target bytes and current pointer, retaining history and enforcing current signer/channel authorization. It is not automatic staging repair. Preserve a current FastTrack waiver only under an authorized root signer; a scoped binding upgrade of a waived head refuses rather than silently dropping the waiver or bypassing signPointer restrictions. An already valid equal binding remains idempotent; a different same-version digest refuses and requires supported restaging/release selection. A malformed present binding never upgrades through a permissive parse. |
| 61 | |
| 62 | Rollback remains a channel pointer operation; it does not query or assert absence of per-host guards. For an unbound legacy current pointer, preserve the existing version-only rollback when the target manifest is missing/pruned. If the target manifest is available, validate it and attach a checked binding when it declares contracts; malformed readable metadata refuses. For a bound current pointer, require the target manifest and construct its binding afresh; unavailable/pruned metadata refuses before pointer mutation. Neither case inherits another version's binding. |
| 63 | |
| 64 | This preserves legacy pruned-target pointer selection without claiming the selected target can serve. Each managed route independently enforces its durable floor during adoption/wake/resume; a legacy unbound rollback cannot satisfy that floor and therefore cannot start or resume the managed instance. Existing maintenance guard phases continue denying admission. Console rollback success means its pointer CAS succeeded, not that a managed route accepted it. This does not invent an app/channel-wide floor-absence authority or a host enumeration API. Tests prove an unbound legacy rollback can still update its pointer while the same selected target is refused by managed admission. |
| 65 | |
| 66 | A retained wrapper's own signed channel is used both when retaining and replacing evidence. A replacement wrapper may use another permitted channel but must independently verify under its own channel and prove identical account/app/version/manifest/artifact. Neither wrapper substitutes for current target-channel selection authorization. Account migration cannot rewrite the account of bound evidence; it must perform an authorized target-account promotion or refuse the contract-bearing migration. |
| 67 | |
| 68 | ## Consumer inventory and gates |
| 69 | |
| 70 | Implementation must cover every pointer writer/reader and staged consumer involved in managed execution, not just public ship/promote: release Promote/PromoteOnLadder/Rollback/FastTrack/signPointer/verifyPointer/Channel; account migrations and direct signing callers; ensureStaged/backfillManifest/StageManifest/ReadStagedManifest and artifact staging; adopt root and system rollback; host wake/materialization and ExecBackend.Seed/Start/StartSidecar, spawnInCgroup, SplitBackend/UnitBackend and their command construction; exact maintenance preflight/execute/resume. Search raw JSON decoding and copies of ChannelPointer and Manifest too. The guard package can remain independent of release; its caller must provide verified declaration evidence, not raw JSON passed as a trusted string set. |
| 71 | |
| 72 | Tests must prove byte-identical legacy fixtures and re-ship, actual root/scoped signature verification, account/app/version/channel/manifest/contract/artifact tamper refusal, missing binding on declared contracts, conflicting staged artifact/evidence and every partial-write recovery order. Include prune-and-reuse with the same version but different manifest/artifact; old proof must fail. Mutations removing digest verification, account binding, artifact verification or required-set comparison must fail named assertions. Contract-bearing promotion, rollback and staged wake must run through actual paths, not only constructors. |
| 73 | |
| 74 | Full native/cross CI, exact hub CI, Fable whole-code review and disabled isolated platform canary remain required. The rollout floor covers console and all lifecycle/record writers as well as serving agents; old readers that ignore additive fields cannot operate managed instances. No production cutover, team activation or maintenance capability is implied by merging this specification. |
| 75 | |
| 76 | Acceptance additionally covers null/duplicate/case-alias/unknown-member wire refusals; unfamiliar but valid contract names; FastTrack preservation; scoped edge evidence reused for stable compatibility but not stable selection; same-head explicit binding upgrade; legacy pruned rollback versus managed refusal; capabilityPlan sibling tampering; all three normal exec paths with staged pathname replacement; fd mode/ownership/close behavior and no fallback; refusal before any capability mutation on an unsupported unit backend. None of these proposed APIs is implemented at the pinned baseline. The approved guard primitives and ValidContracts now exist in main11a; release bindings, opaque verification, managed normal execution and lifecycle-floor plumbing remain implementation work requiring whole-code review and actual runtime tests. |
| 77 | |