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:
| Value | Meaning |
|---|---|
absent / None | This layer supplies no wall-time constraint. |
0 | The activation receives no wall time. |
| positive value | A 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
| Concept | Rust/domain | Protobuf | JSON Schema | WIT | SDKs |
|---|---|---|---|---|---|
| Resource ceilings | ResourceBudget | both ResourceBudget messages | capsule/deployment limits | context resource-budget | Rust, Go, TypeScript, .NET, Java, C ResourceBudget |
| Caller deadline | ActivationEnvelope.deadline_unix_millis | InvokeRequest.deadline_unix_millis | not persistent | context deadline | invocation options / guest context |
| Platform detail | Vec<ErrorDetail> | repeated detail_items | not a declarative resource | list<error-detail> | typed list/array, never a flattened map |
| Terminal outcome | ActivationOutcome / ActivationStatus | invocation/status oneofs | not a declarative resource | invocation-outcome | InvocationOutcome and retained status unions |
| Route identity | InvocationTarget / tenant fields on routes | tenant fields on service/binding routes | required route tenant fields | tenant option on direct service target | generic target always carries tenant |
| Local upload | CapsuleArtifact and bounded typed descriptor codec | CapsuleArtifactUpload | ReleasePublish.artifact | not guest-visible | generated 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; orplatform_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; andcontract_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 generation | Apply | Delete |
|---|---|---|
| Absent | Unconditional upsert. | Delete an existing object; missing is NotFound. |
0 | Require absence, then create. | Existing object conflicts; absent object is NotFound. |
| Positive | Require 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/method | Phase 1 standalone behavior |
|---|---|
ReleaseService publish/get/list | Supported locally. |
DeploymentService apply/get/list/delete | Supported locally. |
DeploymentService.WatchDeployment | Explicitly unimplemented. |
RouteService.GetRouteSnapshot | Supported for the current tenant-scoped snapshot; noncurrent generations are NotFound. |
RouteService.WatchRouteSnapshots | Explicitly unimplemented. |
NodeService.GetNode / ListNodes | Supported for the local node inventory only. |
NodeService.RegisterNode | Explicitly unimplemented. |
NodeService.ReportInventory | Explicitly unimplemented. |
NodeService.Heartbeat | Explicitly unimplemented. |
ContractService, CapabilityService, AuditService, BindingService, TriggerService, PolicyService | Explicitly 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 shape | Replacement | Protection |
|---|---|---|
wall_deadline_unix_millis field 3 | wall_time_limit_millis field 12 | Field 3 and its name are reserved in both Protobuf budget messages. |
PlatformError.details map field 4 | repeated typed detail_items field 5 | Field 4 and its name are reserved. |
InvokeResponse.error field 6 | declared_error field 8 and platform_failure field 9 | Field 6 and its name are reserved. |
CancelResponse.accepted boolean field 1 | enum disposition field 2 | Field 1 and its name are reserved. |
| unscoped route services/bindings | required tenant fields | Schema 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.