carlos / platform Public

Specify verified runtime contract execution and writer edge cases

Paul Campbell pushed by paul@keymail.dev 0a3b84ac8632eb3fd18ec0c774f24fa181f683cc parent 11a3039
1 file changed, +68 −0
  • implementation/runtime-contract-signing.md +68 −0

CI passed — run details

diff --git a/implementation/runtime-contract-signing.md b/implementation/runtime-contract-signing.md
new file mode 100644
index 000000000..b115541d9
--- /dev/null
+++ b/implementation/runtime-contract-signing.md
@@ -0,0 +1,68 @@
+# Signed runtime-contract bindings
+
+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.
+
+## Existing source and the gap
+
+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.
+
+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.
+
+## Additive wire fields
+
+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. 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.
+
+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.
+
+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.
+
+## Signer construction and all pointer writers
+
+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.
+
+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.
+
+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.
+
+## Verification and staged evidence
+
+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.
+
+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.
+
+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.
+
+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.
+
+## Normal execution handoff
+
+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.
+
+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 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.
+
+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.
+
+
+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.
+
+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.
+
+## Writer edge cases and legacy behavior
+
+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.
+
+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. 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.
+
+Rollback outside any managed required-contract floor and from an unbound legacy pointer retains the existing version-only/pruned-target behavior. If a target manifest can be read and declares contracts, it must receive a checked binding; malformed readable metadata refuses. A missing/pruned target can retain legacy pointer behavior only when the caller has affirmatively established that no managed floor applies. Missing floor authority is not that proof. When a current binding or required floor exists, rollback must fetch and validate the target and construct its binding afresh; unavailable/pruned metadata refuses before pointer mutation. This intentional restriction on managed rollback is necessary evidence, not a claim that old history records contain manifests. The caller's required floor is an authenticated lifecycle dependency, not a CLI boolean.
+
+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.
+
+## Consumer inventory and gates
+
+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.
+
+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.
+
+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.
+
+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.