Skip to main content
Version: 0.1.0-alpha.4 (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.

AuditService.QueryPhase2Audit requires explicit query scope and returns typed records, bounded pagination and coverage. Node-wide queries require the trusted operator claim. The obsolete untyped audit projection has been removed. Omission of audit configuration leaves this optional service unavailable.

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.

Explicit publication references (#267)​

The additive PublicationRef carries an exact ID and tenant. Release Get and lifecycle requests add it at field 2; lifecycle changes and renewal add it at field 5. Release descriptors add publication/package fields 13/14, lifecycle records add publication field 12 and release operation receipts add field 14. Audit identity field 18 records the captured publication ID. Existing component fields retain their meanings, and legacy receipts keep their canonical bytes.

Deployment adds publication at 11 and output-only requested_publication at 12. Apply adds an optional component assertion at 4, and managed operation receipts add the captured publication at 17. Requests choose one selector; results may describe component and publication together. Old generated clients ignore new result fields and can still address a unique component association. New clients sending exact selection to an older server cannot fall back: the legacy component selector is empty and the older server rejects the request.

See public selection and recovery for ambiguity, unscoped local compatibility, original manifest preservation and the deployment operation-table upgrade. The descriptor golden deliberately records these additions without changing any existing field number or RPC signature.

StartRollout adds optional candidate component assertion field 8. RolloutRelease adds optional captured publication field 4, and RolloutOperationReceipt adds base/candidate publication fields 20/21. AuditIdentities adds base/candidate publication fields 19/20. InvokeResponse adds optional publication field 10; its component field 3 and generation field 4 retain their meaning. These result IDs describe captured sources and never provide execution authority. Present invalid IDs cannot be normalized to absent legacy values. All six SDK interfaces include corresponding presence-aware models and executable contract fixtures.

Phase 3 capability policy control addition (#203)​

latent.control.v1.PolicyService now has a concrete bounded implementation. All original field numbers/types and RPC signatures remain unchanged. Additions identify the closed policy language, policy/provider-binding record kind, content digest and revocation state; mutations carry explicit operation IDs and receipts. ListPolicies adds scoped opaque pagination and GetPolicyOperation adds bounded historical outcome recovery. Explanation adds exact service, publication, capability operation, typed resource and required policy/binding IDs.

The previously descriptive generic policy requests do not constitute a supported executable language. The concrete profile requires kind/language, operation ID and present expected generation; unsafe legacy omissions reject. Old generic explanation subject/action/resource/attribute overrides must be empty, because identity comes from the trusted transport context. No existing field is silently reinterpreted as publication identity. Full semantics and limits are in durable capability policies.

The normalized descriptor baseline deliberately records these additive changes. They do not change the guest WIT ABI or install a new provider.

Phase 3 capability audit and inspection (#210)​

The descriptor baseline records additive capability audit and inspection fields. AuditIdentities field 21 carries bounded, redacted capability provenance; AuditControlAction value 10 and Phase2AuditEventKind values 16 through 18 identify capability calls, grant decisions and provider outcomes. New typed context, revision, digest and outcome messages distinguish observed provider acceptance from audit durability. Historical records omit the new context and retain their canonical bytes. Existing field numbers and types are unchanged.

CapabilityService keeps its RPC signatures. List request fields 4/5 select a deployment and optional node usage; response fields 3 through 6 expose revision, tenant/node counters and sampled state. Descriptor field 6 adds binding inspection. Explain request fields 6/7 provide a bounded typed resource and optional hypothetical subject; response fields 5 through 8 expose revision, binding inspection, required-audit policy and ceilings.

The formerly descriptive surface now has a closed executable profile: List requires a deployment, tenant scope comes from authentication, and shared node usage requires the trusted operator claim. Explain rejects the legacy principal and attribute overrides; hypothetical subjects inherit the authenticated tenant and cannot supply claims. Descriptions and sampled currentness are not grants. The capability audit and inspection contract defines redaction, pagination, resource ownership and failure behavior. These additions do not change the guest WIT ABI.

Phase 3 HTTP trigger control (#223)​

TriggerService now implements a closed HTTP profile. Existing RPC signatures and field numbers are unchanged; DeleteTrigger still returns Empty with bounded receipt/audit metadata. TriggerTarget fields 5?7 add explicit publication, content revision and deployment object generation. Apply/Delete add an operation ID and present global-state CAS. Responses add receipts, state/route generations, catalog durability and audit acknowledgement; GetTriggerOperation adds bounded historical reconciliation. A trigger's output generation is not a mutation CAS.

AuditIdentities fields 22/23 and action values 11/12 identify trigger operations and object versions. Historical audit records omit these additions and retain their canonical bytes. The descriptor baseline deliberately records the additive changes. This delivery changes no guest WIT definitions.

Profile-less Phase 1 trigger declarations remain structurally decodable but are not executable. The concrete HTTP profile requires all target pins, six closed configuration fields and explicit CAS; it never reinterprets a component digest as a publication selector. See the HTTP trigger contract for authorization, finite ownership, replay, recovery and listener boundaries.

Phase 3 first-class static web targets (#495)​

TriggerTarget adds the TriggerTargetKind discriminator at field 8 while preserving application fields 1 through 7. Both variants require an explicit discriminator; omitted, unspecified and unknown kinds are rejected. STATIC_WEB carries only the exact publication and rejects application-only fields, so older buffered-v1 clients retain their wire shape and new static targets cannot synthesize deployment or component identity.

Trigger operation receipts add the tagged TriggerReceiptTarget at field 21 and its closed target-kind enum. Obsolete format-v1 fields 14 through 18 and their names are reserved after removal. Only format-v2 tagged receipts are accepted; static receipts have no application execution identity. There is no compatibility reader or automatic upgrade. Current field numbers and RPC signatures remain unchanged. The normalized descriptor golden deliberately records the new enums, messages, and fields. See ADR-0043 and the HTTP trigger reference.

AuditIdentities.static_web at field 24 adds the exact static web manifest, asset-layout digest and positive web generation. Static trigger records preserve their publication and trigger identities and omit component, deployment and revision identities. An untouched application-route generation of zero remains valid for static-only nodes. Older application audit records omit the new field and retain their bytes; no existing field or enum is renumbered. The descriptor golden deliberately includes this additive message and field.

Phase 3 componentless web control (#226)​

The descriptor baseline adds six ReleaseService methods: PublishWebPackage, GetWebPublication, GetWebOperation, ChangeWebLifecycle, RenewWebEvidence and PrepareWebPublication. All existing descriptors, field numbers, enum values and RPC signatures remain unchanged. The web methods use their own typed request/response messages rather than reinterpret legacy component selectors.

WebOperationReceipt records publication, authenticated actor, exact operation ID, action/reason, expected/resulting lifecycle generations, request digest and replay state. The three mutation response types preserve operation at field 1 and audit_ack at field 2; their payload bytes and owned sizes match the common preflight representation. Web lifecycle state remains distinct from sampled eligibility and the optional renderer descriptor.

Preparation requires the exact publication, positive lifecycle generation and an explicit bounded wait. Its response reports the selected component and ready state, not a capsule admission grant or activation permission. Web management does not by itself enable Angular T1, additional renderer imports or providers. The management reference defines the trust fence, finite ownership and uncertainty boundary.

Alpha removal of component-only release selectors​

Release get, lifecycle inspection, revoke/retire and evidence renewal now require an exact authenticated-tenant PublicationRef. Their obsolete request digest field 1 is removed and reserved by number and name. The CLI requires --publication ID; positional component digests fail before dispatch. An old wire message containing only field 1 decodes without a publication and is rejected rather than resolved through a unique-component fallback.

Output component digests remain checksums. Publication/operation identities, mutation preconditions, tenant isolation and current authority checks remain explicit. See the current publication API. This alpha change supersedes the earlier release-selector compatibility record; no deprecation interval or obsolete client compatibility is promised.