Register explicit scope/name coordinates independently of Definition type. Require provider-attested current scope snapshots for fetch and dispatch, compare optional scope_ref at membership precedence, and retain the same Descriptor through result validation. Migrate FS pinned-root boundaries and public self-scope example; document provider atomicity/lifecycle and root-only FS support. Add lifecycle, ref, replacement-race, root-lineage and compile-time exposure regressions. Validation: cargo fmt --all --check; git diff --check; cargo check --workspace --all-targets; cargo test --workspace --all-targets and --doc (232 passing test-harness cases including doctests); nix develop --command cargo clippy --workspace --all-targets -- -D warnings; RUSTDOCFLAGS=-Dwarnings cargo doc --workspace --no-deps. Narrow wip-host suite also passed.
9.4 KiB
Structured Interface reference and scope publication migration
This unreleased API/wire change follows wip-reference canonical Core Model,
Protocol and WIP over HTTP at 6086f3c5ef10aa1464ce9c824750d7f667217bfd.
It replaces the earlier opaque Interface reference contract; HTTP routes and
error codes have not changed. Peers must upgrade together: the codec does
not accept legacy strings or display shorthand as Protocol references.
Rust API
Object.interfaces is now Vec<InterfaceReference>.
FetchInterfaceRequest.interface, FetchInterfaceResponse.interface, and
InterfaceTarget.reference all use the same record:
use wip_protocol::{FetchInterfaceRequest, InterfaceReference, InterfaceTarget};
let reference = InterfaceReference {
scope: "/github".into(),
name: "issue.read".into(),
};
reference.validate()?; // reference shape, no target Object path needed
let fetch = FetchInterfaceRequest { interface: reference.clone() };
let target = InterfaceTarget {
reference,
scope_ref: Some("opaque-scope-object-ref".into()),
validator: None,
};
# Ok::<(), wip_protocol::ValidationError>(())
Construct scope and name explicitly from your Worldspace publication context.
There is no implicit conversion from a string, delimiter parser, normalization,
case folding, or URL decoding. A name may contain /, ., ::, #, or Unicode;
only an empty name is invalid. Scope must be a canonical absolute Worldspace
path: /, or nonempty slash-separated segments other than . and .., with
no empty segments or trailing slash. %2F is ordinary field text, not an escape
for a Worldspace path separator.
Equality, hashing and ordering operate on both fields, independent of member order/JSON escape spelling. Use the record itself as a membership/cache key; identical names in different scopes are different references. The descriptor still contains only format, documentation, types and operations, not identity.
FetchInterfaceResponse and InterfaceTarget additionally require the Rust
field scope_ref: Option<String> in struct literals. Use None when omitted,
not an invented identity. This is an opaque scope Object ref observation/call
precondition, not a third Interface identity component or a validator.
Host now enforces scope publication lifetimes and optional scope-ref identity
inside the provider boundary (T-21). Client cache-recovery semantics remain
follow-up work (T-22); do not mistake registry artifact retention for publication.
Validation boundaries
InterfaceReference::validate()andFetchInterfaceRequest::validate()check reference shape independently of a target Object path.Object::validate()checks shape and duplicate(scope, name)pairs;Object::validate_at_path()additionally checks placement, including scope.ObserveRequest::validate_response()checks every derived node path, including the depth boundary. Invalid success responses are Client-localInvalidResponse, not Host-returned Protocol errors.InterfaceReference::validate_for_path(path)checks ancestor-or-self scope compatibility by path segments. Root and self scope are valid;/ais not an ancestor of/ab. This has no indexability dependency.CallOperationRequest::validate()checks descriptor-independent shape.validate_with()adds operation/argument checks but does not replace Host publication lookup. Host interface selection checks scope compatibility and exact membership after target lookup: invalid reference shape isInvalidRequest, but a valid reference outside the target's scope isInterfaceMismatch, withNotFoundretaining precedence.
HTTP wire
All four reference positions use an exact object with required string fields:
{"scope":"/github","name":"issue.read"}
For example:
{
"interface": {"scope":"/github","name":"issue.read"},
"scope_ref": "opaque-scope-object-ref",
"descriptor": {"format":"wip-interface/1","types":[],"operations":[]}
}
A call uses:
{
"target": {"path":"/github/issues/123"},
"interface": {
"reference": {"scope":"/github","name":"issue.read"},
"scope_ref": "opaque-scope-object-ref"
},
"operation":"refresh",
"arguments":{}
}
Unknown/duplicate members, missing fields, null fields, relative scopes, empty
names, legacy "interface-v1" and shorthand "/github::issue.read" strings
are rejected. Field order and JSON escaping do not affect decoded identity.
scope_ref is a normal opaque string, not Base64; omission is allowed but
explicit null and nonstrings are invalid. An empty string is a valid opaque
ref. Validators remain canonical padded standard Base64.
Descriptor-independent HTTP call metadata decoding enforces reference shape
and the same strict JSON rules without turning scope compatibility into early
InvalidRequest. Normal descriptor-bound encode/decode uses the same reference
codec. Request violations retain the existing 400 invalid_request boundary;
malformed success responses retain Client-local InvalidResponse. Standard
POST v1/observe, v1/fetch_interface, v1/call_operation, error-code/status
mapping and Transport failure classification are unchanged.
Host registration and provider migration
Registration now requires an explicit canonical Scope and local name:
let mut registry = wip_host::InterfaceRegistry::new();
let scope = wip_host::Scope::new("/service")?;
let api = registry.register(&scope, "catalog", CatalogDefinition, None)?;
// INSIDE the backend transaction/lock/stable-handle boundary:
let current = wip_host::ScopeObservation::new(&scope, ¤t_scope_object);
let publication = wip_host::PublishedObject::new(current_target_object)
.expose(&api)?
.bind_scope(current);
Collision keys are exact (scope, name) coordinates, not Rust Definition TypeId.
The same Definition can be registered in different scopes or under different
names. Root, self and ancestor scopes are valid, but ancestor compatibility and
explicit target membership remain independent checks. Descriptors still have
no publication identity fields.
A Scope carries a private publication lifetime, not a generated Protocol ref.
Clone it only while the same scope Object remains published: ordinary Object
state/validator updates do not retire it. Deletion, replacement or movement
requires retiring the old lifetime with registry.remove_scope(&scope) and
creating a fresh Scope before republication, even at the same path or ref.
Retained registration tokens cannot fetch or start calls after retirement.
Ref-less replacement cannot be tracked by a Client identity precondition; refs
and scope_ref remain optional, not forcibly generated.
ScopeObservation::new(&scope, &object) is a provider attestation that this
lifetime currently belongs to the Object at the scope path. Construct and attach
it with PublishedObject::bind_scope inside the same coherent boundary as target
resolution, membership, Descriptor/validator selection, implementation and
backend handles, through dispatch and commit/rollback. A matching ref or an
independent scope prelookup is not sufficient. The scope Object need not expose
any interfaces. Providers own atomicity and external-writer limitations; the
runtime checks the attached lifetime and optional scope_ref, and validates both
arguments and results against the same frozen Descriptor. A supplied scope_ref
mismatch, including a missing current ref, is InterfaceMismatch at membership
precedence, after target existence and a supplied Object validator mismatch.
Omission applies no scope identity precondition.
Artifact-only registry.fetch_interface(reference) is removed. Prefer
ObjectProvider::fetch_interface(&provider, ®istry, &reference), which resolves
the visible scope Object with with_object and uses its attached current binding.
Low-level registry.fetch_interface(&reference, ¤t_observation) requires
the same coherent current attestation, not a cached snapshot. Fetch returns
Descriptor, Interface validator and optional scope Object ref together; Clients
need not additionally fetch an ancestor Object. registry.scopes() may repeat
a handle for several names; do not treat it as a unique-scope iterator.
PublishedObject::protocol_object() is a path-free composition projection, not
publication authority. For current observations use ObjectProvider::observe_object
or publication.protocol_object_at(path) inside the coherent provider boundary;
the latter also validates compatible live scope associations and response shape.
Downstream status
The filesystem consumer registers file.read, symlink.inspect and file.write
at its deployment-pinned root Scope. It binds that lifetime from the refreshed
retained root lineage inside the backend execution boundary for calls, generic
Object observations and fetch. A textual Linux root rename/replacement does not
substitute the pinned root Object. The root has no Object ref, so fetch omits
scope_ref; the private lifetime check still applies. Non-root FS registrations
fail closed pending broader FS/TUI semantics integration (T-23).
The public API example self-scopes catalog, audit and diagnostics at
/article-service, fetches through its provider, and carries the observed scope
ref in calls. Client keys use structured values; scope deletion/replacement
cache recovery remains T-22. This Host change does not modify TUI formatting;
canonical Text View work is tracked separately in T-24. Interface authoring/macros remain
descriptor-only and need no identity fields or declaration changes.