250 lines
13 KiB
Markdown
250 lines
13 KiB
Markdown
# Structured Interface reference and scope publication migration
|
|
|
|
## Transport-independent Client API
|
|
|
|
The default `wip-client` feature set still exposes the HTTP-compatible
|
|
`wip_client::Client` and `PreparedRequest`. Its constructor takes state and wire
|
|
limits, sessions canonicalize HTTP endpoints, and `complete` consumes an HTTP
|
|
response. Existing TUI and native HTTP consumers need no source migration; the
|
|
wire contract is unchanged.
|
|
|
|
For an in-process Host (including Client and Host in one WASM module), disable
|
|
default features and use `wip_client::direct::{Client, Request, Response}`:
|
|
|
|
```toml
|
|
wip-client = { version = "0.2.0", default-features = false }
|
|
```
|
|
|
|
- `direct::Client::new(ClientLimits)` takes no wire limits.
|
|
- `open_session(worldspace_identity, SecurityContext)` accepts an opaque identity,
|
|
not a URL. Issue a separate identity per Worldspace view/Host lifetime and a
|
|
separate security identity whenever authorization context changes. Direct and
|
|
canonical HTTP session identities cannot alias each other's cache.
|
|
- Preparation methods retain their names and arguments. Their result is
|
|
`direct::PreparedRequest { id, request: Request }`, with `Observe`,
|
|
`FetchInterface`, and `CallOperation` variants containing Protocol request DTOs.
|
|
- Route that exact DTO to the session's Host/security context; mark calls
|
|
dispatched before invoking the Host. Complete with the original `id` and the
|
|
matching `Response` variant containing `Result<Protocol DTO, ProtocolError>`.
|
|
No HTTP request, response, JSON codec, or artificial endpoint is needed.
|
|
- Direct responses undergo the same semantic validation and state transitions
|
|
as HTTP responses. Wrong interactions and invalid results are rejected;
|
|
invalid call responses retain an unknown execution outcome. Retrievals may
|
|
be explicitly refreshed, but operations are never automatically retried.
|
|
- `ClientLimits` still bounds cache, sessions, in-flight requests, and history.
|
|
HTTP status/envelope checks and byte/JSON nesting limits belong to the optional
|
|
HTTP adapter. An embedding application owns DTO allocation/execution budgets.
|
|
|
|
The `direct` module API documentation contains a runnable typed example. Run
|
|
`cargo test -p wip-client --no-default-features` to exercise the core without an
|
|
HTTP dependency; ordinary `cargo test -p wip-client` also runs unchanged HTTP
|
|
regressions and direct/HTTP state parity tests.
|
|
|
|
|
|
The 0.2.0 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:
|
|
|
|
```rust
|
|
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 enforces scope publication lifetimes and optional scope-ref identity
|
|
inside the provider boundary. Client caches keep the complete structured reference
|
|
and optional fetched scope binding; confirmed scope deletion or ref replacement
|
|
invalidates bound observations, while local eviction is not deletion evidence.
|
|
Do not mistake registry artifact retention for publication.
|
|
|
|
## Validation boundaries
|
|
|
|
- `InterfaceReference::validate()` and `FetchInterfaceRequest::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-local
|
|
`InvalidResponse`, 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; `/a` is 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 is
|
|
`InvalidRequest`, but a valid reference outside the target's scope is
|
|
`InterfaceMismatch`, with `NotFound` retaining precedence.
|
|
|
|
## HTTP wire
|
|
|
|
All four reference positions use an exact object with required string fields:
|
|
|
|
```json
|
|
{"scope":"/github","name":"issue.read"}
|
|
```
|
|
|
|
For example:
|
|
|
|
```json
|
|
{
|
|
"interface": {"scope":"/github","name":"issue.read"},
|
|
"scope_ref": "opaque-scope-object-ref",
|
|
"descriptor": {"format":"wip-interface/1","types":[],"operations":[]}
|
|
}
|
|
```
|
|
|
|
A call uses:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```rust,ignore
|
|
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 default registers local names `file.read`, `symlink.inspect` and
|
|
`file.write` at its deployment-pinned root Scope, displayed as `/::file.read` etc.
|
|
External compositions choose explicit Scope/local names and implement
|
|
`Composition::scope` to attest a non-root lifetime from the exact refreshed backend
|
|
entry in its connected lineage. The engine never independently resolves a scope
|
|
path during dispatch. Default composition remains root-scoped; exposing non-root
|
|
registrations requires the application's current lifetime attestation, not merely
|
|
an artifact or matching ref/validator. All bindings are formed inside backend
|
|
execution, with current principal lineage visibility and publication metadata.
|
|
Retire/recreate Scope handles on deletion, replacement or movement; a pinned leaf
|
|
alone is not evidence that a self-scope Object remains published at that path.
|
|
The Linux deployment root remains pinned across textual root rename/replacement,
|
|
and its ref-less default publication continues to omit scope_ref.
|
|
|
|
The public API example self-scopes `catalog`, `audit` and `diagnostics` at
|
|
`/article-service`, fetches through its provider, and constructs call metadata
|
|
from the fetched structured reference and scope_ref. External-style conformance
|
|
tests also cross real TCP HTTP for root/self-scopes, same names in different
|
|
scopes, optional refs, stale refs, deletion/republication and validator precedence.
|
|
|
|
The TUI stores structured references in cache keys, selections, actions, editors
|
|
and in-flight subjects. Normal screens reuse the canonical absolute scoped
|
|
formatter (`/scope::name#operation`, with JSON component quoting when needed).
|
|
Labels are never parsed to reconstruct identity, and scope_ref/validators are not
|
|
normal-screen reference labels. Client invalidation is synchronized back into UI
|
|
observations before dispatch; re-observation is not automatic execution. Typed
|
|
result entries are still fetched on selection, not all at once, and do not create
|
|
inferred indexable edges. Interface authoring/macros remain descriptor-only.
|