Skip to main content
Version: 0.1.0-alpha.3 (alpha)

Phase 1 contract hardening

Issue #36 defines the pre-stabilization contract changes that every Phase 1 implementation must consume. This document is normative for the affected Rust models, Protobuf APIs, JSON schemas, WIT packages, and SDK surfaces.

Budget and deadline semantics​

ResourceBudget is a set of hard ceilings. Every numeric value is an exact limit, so 0 means that no amount of that resource is granted; it never means "use a platform default." This avoids an omitted/zero ambiguity across a request, deployment, and node policy.

wallTimeLimitMillis / wall_time_limit_millis is the one optional member:

ValueMeaning
absent / NoneThis layer supplies no wall-time constraint.
0The activation receives no wall time.
positive valueA relative maximum measured from admission.

Persistent capsule, deployment, and node documents must use only this relative field. wallDeadlineUnixMillis is not valid in a schema and Protobuf field number 3 is reserved so it cannot be repurposed. A caller's absolute deadline appears only on InvokeRequest.deadline_unix_millis and ActivationEnvelope.deadline_unix_millis.

At admission, the granted resource counters are the element-wise minimum of the request, deployment, and node ceilings. The effective absolute deadline is the earliest of the caller deadline and each configured relative limit measured from that admission time:

min(caller_absolute_deadline,
admission_time + request.wall_time_limit,
admission_time + deployment.wall_time_limit,
admission_time + node.wall_time_limit)

Missing values are omitted from the minimum. Arithmetic saturates only at the maximum representable Unix millisecond value, never to a shorter accidental deadline.

Cross-layer representation audit​

ConceptRust/domainProtobufJSON SchemaWITSDKs
Resource ceilingsResourceBudgetboth ResourceBudget messagescapsule/deployment limitscontext resource-budgetRust, Go, TypeScript, .NET, Java, C ResourceBudget
Caller deadlineActivationEnvelope.deadline_unix_millisInvokeRequest.deadline_unix_millisnot persistentcontext deadlineinvocation options / guest context
Platform detailVec<ErrorDetail>repeated detail_itemsnot a declarative resourcelist<error-detail>typed list/array, never a flattened map
Terminal outcomeActivationOutcome / ActivationStatusinvocation/status oneofsnot a declarative resourceinvocation-outcomeInvocationOutcome and retained status unions
Route identityInvocationTarget / tenant fields on routestenant fields on service/binding routesrequired route tenant fieldstenant option on direct service targetgeneric target always carries tenant
Local uploadCapsuleArtifact and bounded typed descriptor codecCapsuleArtifactUploadReleasePublish.artifactnot guest-visiblegenerated management RPC clients; handwritten SDK surfaces remain invocation-focused

The one intentional target difference is WIT direct service invocation: target.tenant = none means the current invocation principal's tenant. The generic external invocation API always requires a tenant. A supplied different WIT tenant requires a future explicit grant; Phase 1 must deny it rather than silently cross a tenant boundary.

Invocation, errors, and status​

Caller activation, root, and parent IDs are optional and are preserved across all six SDK request surfaces. Their absence, lineage validation, known-ID cancellation/status, local-wait cancellation, and compatibility rules are specified in the SDK contract. The server assigns missing activation IDs; SDKs do not silently generate them or reinterpret a present empty ID as absent. This does not add an automatic retry policy or make an activation ID an idempotency key.

An invocation has exactly one wire-visible terminal result:

  • success: a guest completed successfully;
  • declared_error: a guest/domain result; or
  • platform_failure: infrastructure rejected, interrupted, or failed it.

Each result includes finalized BudgetConsumption. A platform error carries an ordered list of { kind, fields } details; a map-only transport shape is not permitted because it loses both the detail kind and repeated detail boundaries.

latent-rpc::platform_error provides the canonical conversion between the Rust PlatformError domain type and both generated Protobuf PlatformError types. Stable codes use the documented lower-kebab-case spelling. Unknown wire codes are rejected with UnknownPlatformErrorCode instead of being coerced to a known classification. The conversion preserves message, retryability, detail order, detail kind, and every bounded field exactly through Prost serialization.

The Rust execution boundary mirrors that distinction before an activation is mapped: GuestOutcome has separate Returned, DeclaredError, Trapped, and Interrupted variants. A backend must convert a typed guest/domain result to DeclaredError at that boundary. The activation runner must not infer a declared error from payload bytes, media types, or a service-specific convention.

Cancellation returns an explicit disposition: accepted, already-terminal, or not-found. Only malformed requests and transport/platform failures use an RPC failure path. GetActivation retains its terminal state, typed terminal outcome, final consumption, and terminal timestamp when known.

For a retained success, the summary carries committed_state_version, effect_ids, and metadata; the Rust domain summary, Protobuf ActivationSuccessSummary, and every SDK retain the same three fields.

Local release publication​

Phase 2 adds PublishReleaseRequest.package at field 3 and the bounded package layer/evidence messages without changing existing field numbers or service signatures. Exactly one of artifact and package is required; package input forbids the optional legacy release descriptor. The checked descriptor golden includes this deliberate additive change. Existing Phase 1 clients keep the local upload representation below; enforced nodes reject it and require the authenticated package path.

Phase 1 PublishRelease uses the bounded unary CapsuleArtifactUpload:

  • capsule_manifest_json;
  • component_bytes;
  • component_digest;
  • component_media_type; and
  • contract_metadata_json, the bounded {"format_version":1,"contracts":[...]} typed descriptor document.

The adapter must impose configured request/artifact byte limits before parsing or storing content, verify the component digest, and derive any local storage locator itself. ReleaseDescriptor.artifact_reference is therefore a server-assigned opaque locator, never a client-visible filesystem path. The JSON analogue is schemas/release-publish.schema.json.

The implemented management adapter requires an explicit capsule tenant matching the authenticated administrator. It verifies typed metadata for every declared export and preserves descriptor parameters, results, and nested value types through the public contract codec. The JSON schema analogue requires a contractMetadataJson string. The checked-in publish example demonstrates that shape with placeholder bytes/digests; it is not an executable upload. Actual generated-client tests use complete tiny catalog publication fixtures.

Deployment generations and pagination​

The deployment repository supplies the atomic versioned port consumed by the implemented management adapter. The generated Deployment.generation field is output-only: adapters ignore it in apply input and preserve its full unsigned 64-bit value in responses. The optional expected_generation field is the caller's only precondition:

Expected generationApplyDelete
AbsentUnconditional upsert.Delete an existing object; missing is NotFound.
0Require absence, then create.Existing object conflicts; absent object is NotFound.
PositiveRequire the exact current object version.Require the exact current object version.

An object version is its last mutation's catalog stamp. Unrelated writes do not change it; unchanged applies do. Delete/recreate receives a new stamp. The repository checks caller expectations at atomic commit and returns the committed record/version directly. Adapters must not substitute read/check/write sequences or read the current object afterward to construct a mutation response. Internal compilation conflicts are separate from stale caller versions: a concurrent unrelated write may require recompilation with the same caller precondition.

Get, list and delete requests obtain their tenant from the authenticated local principal; the wire messages' missing tenant fields do not authorize global catalog access. An apply's manifest tenant must match that principal. Adapters must use the tenant-scoped repository operations, bound input and response sizes, and map structured errors without exposing another tenant's state.

Deployment pages are ordered by deployment ID within one tenant and optional service filter. Tokens are opaque and bound to that scope and repository generation; any catalog publication or repository reopen expires them. They do not authenticate or authorize callers. Limits and token validity are checked before selecting or cloning records, and pagination does not fetch artifacts. Changing page size between continuations is permitted within the configured limits. The repository rejects page size zero. In #37's wire adapter, a missing PageRequest or zero page_size selects the adapter's configured positive default within the repository limit: Protobuf cannot distinguish an omitted non-optional scalar from explicit zero. The repository's page byte budget covers encoded records; adapters also bound the entire wire response. Generated-client/ server tests over an in-memory transport cover generation conversion, preconditions, scoped pagination, and unsupported watches against real small directory catalogs. The adapter does not expose a management listener.

Tenant-qualified routes​

Service IDs are not globally unique. Every compiled ServiceRoute has a required tenant, and every BindingRoute has consumer_tenant and provider_tenant. Resolution keys include the tenant before service, route, contract, or function selection. A route compiler must reject or keep separate same-named services from different tenants; it must not rely on a convention in the service ID string.

Standalone Phase 1 RPC subset​

The invocation adapter (#12) and management adapter (#37) implement their standalone subsets in latent-wire. The standalone node (#14) supplies their configured loopback listener and process composition. Generated messages and service traits alone do not make the other control-plane services callable.

Service/methodPhase 1 standalone behavior
ReleaseService publish/get/listSupported locally.
DeploymentService apply/get/list/deleteSupported locally.
DeploymentService.WatchDeploymentExplicitly unimplemented.
RouteService.GetRouteSnapshotSupported for the current tenant-scoped snapshot; noncurrent generations are NotFound.
RouteService.WatchRouteSnapshotsExplicitly unimplemented.
NodeService.GetNode / ListNodesSupported for the local node inventory only.
NodeService.RegisterNodeExplicitly unimplemented.
NodeService.ReportInventoryExplicitly unimplemented.
NodeService.HeartbeatExplicitly unimplemented.
ContractService, CapabilityService, AuditService, BindingService, TriggerService, PolicyServiceExplicitly unimplemented; these service adapters are outside the completed Phase 1 standalone subset.

An adapter must return its transport's standard unimplemented status for every listed unsupported method; it must never return an empty successful response.

Compatibility record​

This is a deliberate pre-Phase-1-stabilization source and wire break. The repository has not released a stable Phase 1 API, and no generated client code is committed. The initial #36 changes removed ambiguity at the Phase 1 foundation boundary:

Prior shapeReplacementProtection
wall_deadline_unix_millis field 3wall_time_limit_millis field 12Field 3 and its name are reserved in both Protobuf budget messages.
PlatformError.details map field 4repeated typed detail_items field 5Field 4 and its name are reserved.
InvokeResponse.error field 6declared_error field 8 and platform_failure field 9Field 6 and its name are reserved.
CancelResponse.accepted boolean field 1enum disposition field 2Field 1 and its name are reserved.
unscoped route services/bindingsrequired tenant fieldsSchema fixtures and descriptor contract tests require the fields.

The #37 implementation adds upload field 5 (contract_metadata_json), explicit release and snapshot tenant fields, and complete node inventory observations without reusing existing field numbers. Older publish callers must supply the new typed metadata even though protobuf decodes an omitted bytes field as empty; the adapter rejects empty metadata. Inventory preserves optional/unmeasured values, cache and topology availability, scheduler acceptance, quotas, pressure, and health. Snapshot digests cover a canonical tenant projection rather than global catalog state. The descriptor golden records these additive wire fields and their presence semantics.

api/proto/phase1-descriptor-contract.json is a normalized, checked-in FileDescriptorSet golden. tools/validate_contracts.sh builds the authoritative descriptor with Buf and tools/validate_phase1_descriptor.py compares field types, cardinality, oneof membership, enum values, service signatures, and reservations with that golden. tools/tests/test_phase1_contracts.py exercises the validator's drift detection and the cross-SDK surface requirements.

Integration boundary​

Phase 2 lifecycle #148 adds PublishReleaseRequest.operation field 4 and PublishReleaseResponse.operation field 3 without reusing or changing existing field numbers. Omission preserves existing publication callers. New unary GetReleaseLifecycle, GetReleaseOperation, ChangeReleaseLifecycle and RenewReleaseEvidence methods expose bounded lifecycle state and operation receipts; actor/tenant authority comes from trusted authentication. Explicit operation preconditions preserve generation presence, including present zero for publication versus required positive generations for later mutations. ReleaseDescriptor.admitted remains historical. See release lifecycle for retention, uncertain outcomes and renewal semantics. Generated Rust struct literals require the additive fields; no generated source or new management-language SDK is checked in.

Phase 2 audit #152 adds AuditService.QueryPhase2Audit with explicit query scope, typed records, bounded pagination and coverage. The existing QueryAudit RPC keeps its original request/response fields and service signature; its optional standalone implementation is a bounded tenant-scoped projection. Node-wide typed queries require the existing trusted operator claim. These services extend the Phase 1 subset described above; omission of audit configuration preserves the unaudited embedding surface.

Optional audit_ack fields are additive: PublishReleaseResponse field 4, ChangeReleaseLifecycleResponse and RenewReleaseEvidenceResponse field 2, and ApplyDeploymentResponse field 3. AuditAck separates persistence status from the authoritative mutation result and preserves presence of its attempt sequence. Delete keeps its existing Empty response; bounded error/Delete acknowledgements use latent-audit-status and optional latent-audit-attempt metadata. Existing field numbers and enum values remain unchanged. The normalized descriptor golden includes these additions; old generated clients may ignore them, while new Rust response literals must initialize their optional fields. See audit semantics for loss, recovery and response ownership.

Phase 2 rollout #153 adds rollout.proto to the exhaustive manifest and its five unary RolloutService methods for start, change, status, listing and operation lookup. Required preconditions preserve optional integer presence: Start requires present rollout revision zero and a positive base object generation; Change requires a positive rollout revision. Responses separate the canonical committed receipt, replay flag, catalog durability and audit acknowledgement. Omitted node enablement returns Unimplemented.

The existing audit identity gains optional rollout revision, step and combined state version, and the attempt gains an optional expected rollout revision. These fields are additive and distinct from lifecycle/deployment generations. Absent fields retain historical audit canonical bytes. The descriptor golden records the new service and fields without changing prior numbers or signatures; see rollout semantics.

Controlled canary promotion (#154)​

The rollout service adds an explicit optional Start policy, a distinct Promote command and an authenticated Evaluate RPC. Existing command tags and field numbers remain unchanged. Missing policy preserves manual rollout semantics; zero-valid thresholds require scalar presence. Diagnostic reports preserve candidate and baseline counts, fixed latency buckets and loss dispositions. Mutation responses separate durable receipts from transient observation availability.

Audit conclusions gain an optional closed decision summary and typed canary identities. Absent additions preserve legacy canonical records. Reports and protobuf messages cannot construct the private observation input accepted by catalog promotion. See canary promotion.

Atomic eligible-release rollback (#155)​

Change adds rollback at oneof tag 15 with a positive target-generation comparison. New target messages preserve the historical generation and retained manifest digest separately from the resulting route generation. Status field 20 exposes the plan-bound target for new plans; receipt field 19 records it only for a committed rollback. RolledBack state, Rollback action and RollbackApplied reason are additive enum values. Older plans without a captured target remain readable but cannot authorize a fresh rollback.

Audit identity field 17 records the validated rollback target generation; attempt field 11 records the caller's expected target independently. Optional absent fields preserve legacy canonical bytes. Existing RPC signatures and field tags are unchanged. See rollback semantics.

Operator workflows and managed deployment recovery (#156)​

Apply/Delete request field 3 optionally selects an audited operation with a caller ID and explicit catalog-state precondition, alongside the existing object-version precondition. Apply adds a compact receipt, replay flag and catalog durability. Delete retains its Empty response and adds bounded operation/audit metadata. GetDeploymentOperation returns a retained receipt or explicit unknown/uncertain coverage; missing history is not evidence that a mutation never ran.

Get request field 2 explicitly requests a coherent managed snapshot, including object absence. Ordinary Get retains its prior store path and limits; an adapter that cannot supply the requested snapshot returns Unsupported. Snapshot response fields and audit attempt field 12 (expected_state_version) are additive. The descriptor golden includes these changes without renumbering existing fields. See operator workflows for replay, authorization, finite retention, local package verification and client recovery semantics.

Phase 0 gate #25 and the executable build foundation in #2 are complete. This work is reconciled with the finalized Phase 0 retained/replaced classification and with development's generated Rust, Component Model, and RPC ownership. The hardened Protobuf services compile through latent-rpc, and the normalized Buf descriptor golden is verified from the same exhaustive Protobuf manifest.

No dependency gate remains on this merged contract work. Changes are checked by the normal CI workflow and the path-filtered Phase 0 runtime regression workflow, including Phase 1 descriptor/SDK checks and retained executable containment coverage. The Phase 0 full completion gate and heavy catalog scale probe require explicit manual selection; see validation.