Files

50 KiB

3. Protocol

WIP Protocolは、ClientとHostが交換するlogical data modelと、Core interactionのrequest / responseを定義する。

Structの記法はRust風だが、Rust固有のmemory layoutやserialization libraryは要求しない。TypeScriptを含む他言語でも、同じ型、必須性、validation semanticsを実装できなければならない。

HTTP endpoint、JSON fieldへのencoding、status codeはTransport Bindingの責務である。共通要件は5. Transport Bindings、標準bindingの定義は5.1. WIP over HTTPにある。

Protocol dataの分類

Clientが取得してcacheするfirst-classなprotocol dataは、次の二つである。

  • Object
  • InterfaceDescriptor

一方、Operationのargumentsとresultには、Interface Descriptorで宣言されたValueを使用する。Application dataは独立したWIP resourceにはせず、Operation callの入力または出力として交換する。

Protocol data
  |-- Object
  `-- InterfaceDescriptor

Operation value
  |-- arguments
  `-- result

Path、Object参照、Interface参照、validator、error等は、現れるfieldやenvelopeに応じて意味が決まる。Protocol上のnamed scalarとしては定義しない。

記法

Logical structの型とfieldは、次の記法で表す。

struct T { ... }       // 名前付きfieldを持つrecord
enum T { ... }         // 閉じたcaseの集合
Option<T>              // 省略可能なfield
Vec<T>                 // 順序付きlist
BTreeMap<String, T>    // string keyを持つlogical map

Option<T>はfieldの省略を表し、nullを表すものではない。Transport Bindingは、省略されたfieldをwire valueへ出力してはならない。

BTreeMapはstring-keyed mapをRust風に示す記法にすぎない。Keyのiteration orderはProtocol semanticsに含めない。

Protocol primitives

Protocol structを表すprimitiveとcompositeは、次のものだけに限定する。

unit
boolean
integer
number
string
bytes
list
record

Path、Interface参照、Object identity、validator、Operation名等を独立したProtocol scalarにはしない。ただし、同じprimitiveを使うfieldでも、その意味を交換可能とはしない。各fieldの定義とvalidation ruleに従って解釈する。

Rust SDKは、利便性や安全性のためにPath等のnewtypeを公開してよい。TypeScript SDKもbrand等を利用してよい。これらは言語別SDKのAPIであり、Protocol structの一部ではない。

Path

Pathとして宣言されたfieldのvalueは、同じWorldspace内のObjectを取得するためのcanonical absolute pathでなければならない。

  • Root pathは/とする。
  • 非root pathは、rootから対象Objectまでの各nameを/で連結して構成する。
  • 空segmentを含めない。
  • .または..であるsegmentを含めない。
  • Root以外は末尾に/を持たない。
  • Pathに含まれる各segmentはUnicode stringとして扱い、Unicode normalizationを行わない。
  • Segmentの比較はUnicode code point sequenceの完全一致で行う。
  • Navigationに使用できるが、恒久的なObject identityとして使用しない。

Nameに/を許可しないため、Protocolはpath segment escapeを定義しない。Transport Bindingが行うURL encodingは、WIP Pathの構成とは別に扱う。

Interface Reference

Interface参照は、scopeとlocal nameを持つrecordである。Objectやinteractionで使う参照のstructであり、新しいfirst-class resourceやnamed scalarではない。

struct InterfaceReference {
    scope: String,
    name: String,
}
  • scopeはClient-facing Worldspaceのrootに基づくcanonical absolute path。上記Pathの規則を適用し、relative path、. / ..、空scopeを認めない。Root scopeは/とする。
  • nameはそのscope内のInterfaceを識別する空でないUnicode string。.、::、#等を含んでもfield内の文字として扱い、pathやOperation名へ分解しない。
  • 同じWorldspace context内でscopeとnameのUnicode code point sequenceがそれぞれ完全一致する場合だけ、同じ参照とする。Normalization、case変換、URL decodeは行わない。
  • Object.interfacesの重複判定、membership、request / responseの参照一致、Interface cacheのkeyにはこの組を使う。Object member順や短縮表記の引用・escapeの違いは参照の同一性へ影響しない。
  • Interface Descriptorはscopeやnameを自身のfieldとして持たない。同じ構造のDescriptorであることから参照の同一性を推測しない。

このstructは、Object.interfacesの各要素、FetchInterfaceRequest.interface、FetchInterfaceResponse.interface、InterfaceTarget.referenceで共通して使用する。Opaque stringやscope::nameを連結したstringを代わりに受理しない。Operation名は、別fieldのCallOperationRequest.operationに保持する。

人間・AI向けの短縮表記は4.2. Text Viewに従う。Scope省略形を入力として受けるadapterも、明示されたcontextから絶対scopeを確定してからProtocol requestを構築する。

参照shapeの不正、relative scope、空name等は、Core request validationでInvalidRequestとする。Responseに不正な参照があれば、Client-localなInvalidResponseとして扱う。参照自体がvalidであることと、そのInterfaceが主体へ公開されていること、対象Objectで使えることは別の条件であり、後者はinteractionごとに検証する。

Documentation

Documentationは、Interface、Operation、type、field、parameter、caseへ付与する説明のlogical structである。

struct Documentation {
    summary: String,
    details: Option<String>,
}

summaryは一覧やInterface signatureに短く表示する説明である。detailsは、説明を明示的に展開したときに使用する。

これらはHostが提供するplain text dataであり、Clientのsystem instructionとして信頼しない。Transport BindingとClientはresource limitを適用できる。Markup、localization、構造化exampleは、現在のProtocolには含めない。

Object

Objectは、一つのpathで現在公開されているprotocol-level projectionである。

struct Object {
    name: String,
    description: Option<String>,
    interfaces: Vec<InterfaceReference>,
    r#ref: Option<String>,
    validator: Option<Vec<u8>>,
}

name

nameは、親Objectの下でObjectを識別するpath segmentである。Root Objectでは空文字列とし、非root Objectでは空であってはならない。Nameに/を含めてはならず、Name全体が.または..であってはならない。

NameはUnicode stringとして扱い、Unicode normalizationを行わない。同じ親のchildrenは、Unicode code point sequenceの完全一致で互いに異なるnameを持たなければならない。

description

descriptionは、Objectが何を表すかを説明する短い公開情報である。Objectの本文やApplication data全体を格納するfieldではない。

interfaces

interfacesは、Objectで利用できるOperationを定義するInterface Descriptorへの参照を保持するordered setである。順序はHostの立項順とする。

  • 集合は空でもよい。
  • 同じInterface参照を重複して含めてはならない。Clientは重複を含むObject responseをHostから返されたProtocol failureではなくInvalidResponseとして扱う。
  • Membershipはset semanticsを持ち、列挙順はOperation解決、validation、authorizationへ影響しない。
  • Clientは列挙順をInterfaceおよびOperationのUI表示順として使用してよい。
  • Operation callは集合から一つのInterface参照を明示する。

異なるInterfaceでは、同じOperation名を宣言できる。Protocol上は構造化参照とOperation名を別fieldで扱い、表示用の短縮表記は4.2. Text Viewに従う。

同じInterface参照のDescriptorを時間とともに変更するHostは、validatorを発行する。Validatorを発行しない参照のDescriptorは、その参照が有効な間は変更しない。Clientはfetch_interfaceでDescriptorとvalidatorを一つの観測として取得する。

ref

refはoptionalなopaque stringであり、同じ主体に公開された一つのWorldspace内で、複数の観測やエントリが同じObjectを表すことを示す。

  • 同じ有効なrefは同じObjectを表す。
  • Hostは同じ有効なrefを異なるObjectへ付与してはならない。
  • 異なるまたは欠落したrefからObjectが異なるとは判断しない。
  • 異なるWorldspace間でrefを比較しない。
  • Session、再接続、Host restart、Worldspace再構築をまたぐ永続性を要求しない。
  • Navigationには使用せず、refからObjectを取得するinteractionは提供しない。

Backendに適切なUIDがある場合、Hostは直接、または変換してrefに使用できる。ただし、Protocolはbackend UIDの存在も公開も要求しない。

validator

validatorは、HostがObjectの現在状態に付与するoptionalなopaque bytesである。Clientは内容を解釈しない。観測に基づくOperationを条件付きで実行する場合は、pathとともにtarget.validatorへコピーする。

生成方法はHostが自由に選べる。Generation、content hash、database revision等を使用してよい。ただし、validatorはObject identityとHostが保護する現在状態の両方に結び付ける。Target pathが別Objectを指す場合は、内容が同じでも一致させてはならない。Content hashを使う場合は、Object identityをhashに含めるか、Hostがidentityを別途検証する。

どの状態変化によってvalidatorを無効にするかは、HostがそのObjectに対して一貫した方針を適用する。Object validatorはpathに固有のものではなく、同じrefを持つObjectの現在状態に付与する。

Object validatorは、Client runtimeがObject observationとともに保持するprotocol metadataである。AIやユーザーへ公開するApplication dataではない。

Objectの「現在状態」とは、Hostがvalidatorによってobservation consistencyを保護する対象を指す。WIP Objectはprotocol-level projectionであり、Application stateを格納する共通containerではない。WIPは状態遷移modelを定義せず、OperationをObject stateに対するread / writeとして分類しない。

同じrefを持つObjectの整合性

同じWorldspace内で同じrefを持つObjectは、一つの共有された現在状態を表す。

  • 同じrefと同じvalidator valueを持つObjectは、validatorが存在する場合、name、description、interfacesを含む全fieldが一致しなければならない。
  • 同じrefと異なるvalidatorを持つObjectは、同一Objectの異なる状態として扱う。
  • HostはObjectが配置されたpathを理由に、同じrefへ異なるname、description、interfacesを同時に公開してはならない。
  • Clientは最後に成功した観測を、そのrefを持つObjectの現在状態として扱う。

同じObjectを複数のpathへ配置する場合も、各配置には同じObjectのnameを使用する。Pathの違いは、異なる親を経由することで生じる。

Object自体はchildrenとpathを直接持たない。これらはObject identityに固有とは限らず、Objectを観測したentryのcontextに属する。そのため、request pathとObjectObservationで表す。

Objectの意味とApplication dataとの境界については、2. Core Modelと2.1. Object tree / Schemaに定義がある。

Interface Descriptor

InterfaceDescriptorは、実装コード上のInterface Declarationから生成するmachine-readable metadataである。

struct InterfaceDescriptor {
    format: String,
    documentation: Option<Documentation>,
    types: Vec<TypeDeclaration>,
    operations: Vec<OperationDeclaration>,
}

生成にはRust macro、TypeScript definition API、code generator等を用い、Hostが公開する。Interface実装者がこのstructのJSON encodingを手書きすることは想定しない。

typesとoperationsのdeclaration順は、表示時に保持してよい。ただし、名前解決やvalidationの結果には影響しない。

Type declaration

struct TypeDeclaration {
    name: String,
    documentation: Option<Documentation>,
    definition: TypeExpr,
}

Type名は、一つのDescriptor内で一意でなければならない。

Type expression

enum TypeExpr {
    Unit,
    Boolean,
    Integer,
    Number,
    String,
    Bytes,
    Json,
    Entry,

    Named {
        name: String,
    },

    Record {
        fields: Vec<FieldDeclaration>,
    },

    List {
        items: Box<TypeExpr>,
    },

    Enum {
        cases: Vec<EnumCase>,
    },

    Union {
        cases: Vec<UnionCase>,
    },
}

Namedの参照先は、同じInterface Descriptor内のTypeDeclarationである。External type importとrecursive type referenceは定義しない。Descriptorの意味検証では、存在しないTypeへの参照と参照cycleを拒否する。

Record

struct FieldDeclaration {
    name: String,
    required: bool,
    documentation: Option<Documentation>,
    r#type: TypeExpr,
}

Field名は、一つのRecord内で一意でなければならない。required = falseで許可するのはfieldの省略であり、nullではない。

また、Record valueに未宣言fieldがある場合はvalidation failureとする。

Enum

struct EnumCase {
    name: String,
    documentation: Option<Documentation>,
}

Enum caseはpayloadを持たない。Case名は、一つのEnum内で一意でなければならない。

Union

struct UnionCase {
    name: String,
    documentation: Option<Documentation>,
    payload: Option<TypeExpr>,
}

Union caseのpayloadは、caseごとに0個または1個とする。Case名は、一つのUnion内で一意でなければならない。

Operation declaration

struct OperationDeclaration {
    name: String,
    documentation: Option<Documentation>,
    parameters: Vec<ParameterDeclaration>,
    returns: ReturnDeclaration,
}

struct ParameterDeclaration {
    name: String,
    required: bool,
    documentation: Option<Documentation>,
    r#type: TypeExpr,
}

struct ReturnDeclaration {
    documentation: Option<Documentation>,
    r#type: TypeExpr,
}

Operation名は、一つのDescriptor内で一意でなければならない。Parameter名は、一つのOperation内で一意でなければならない。

Interface Descriptorの生成と意味論は2.2. Operation and Interface、人間とAIへのInterface signatureは4.2. Text Viewに従う。

Value

Valueは、Operation argumentsとresultを表すlogical valueである。Schema上の意味は、この型自体には含めない。

enum Value {
    Unit,
    Boolean(bool),
    Integer(i64),
    Number(f64),
    String(String),
    Bytes(Vec<u8>),
    Record(BTreeMap<String, Value>),
    List(Vec<Value>),
}

Valueが表すのは、HostとClientが交換するprimitiveとcompositeだけである。Path、enum case、union case等のsemantic variantは持たない。既知のInterfaceに対するgenerated bindingは、言語固有DTOとValueのcodecを提供できる。

Primitive semantics

Value 意味 制約
Unit 値を持たない Optional fieldの省略とは異なる
Boolean 真偽値 —
Integer 符号付き整数 -9007199254740991から9007199254740991まで
Number IEEE 754 binary64 NaN、正負Infinityを禁止する
String Unicode string —
Bytes 任意のoctet列 Transport Bindingがencodingを定める
Record String keyを持つvalueの集合 Key順に意味を持たない
List 順序付きvalue —

integerの範囲は、RustとTypeScriptの間でlosslessに相互運用できる範囲に制限する。

numberを受信したruntimeは、有限値であることを検証する。

Descriptorによる解釈

RawなValueの意味と追加validationは、Interface DescriptorのTypeExprによって決まる。

TypeExpr 対応するValue 追加の解釈
Unit Value::Unit 値を持たないresult等
Boolean Value::Boolean —
Integer Value::Integer 相互運用範囲を検証する
Number Value::Number 有限値を検証する
String Value::String 通常の文字列として扱う
Bytes Value::Bytes Octet列として扱う
Json JSON data modelで表現可能なValue 内部構造をWIP schemaとして解釈しない
Entry Value::String Canonical absolute Worldspace pathとして検証する
Record Value::Record Field declarationに従って検証する
List Value::List 各要素をitems typeに従って検証する
Enum Value::String 宣言されたcase名として検証する
Union Value::Record Discriminatorとpayloadをcase declarationに従って検証する

Jsonが受け付けるのは、JSONのnull、boolean、number、string、array、objectに対応するValueだけである。Bytesは含まない。JSON内の整数と数値にも、Value::IntegerとValue::Numberと同じ範囲および有限性の制約を適用する。

EntryとStringは、同じprimitiveで交換する。Worldspace pathとして扱うのは、Descriptor上でEntryと宣言された位置だけである。Clientは通常のStringを、その内容からEntryへ変換しない。

同様に、EnumはString、UnionはRecordとして交換する。SDKやruntimeは、Descriptorによるvalidationを終えた内部表現としてtyped enum、union、Path等を構築してよい。ただし、これらはProtocolの送受信形式ではない。

Value validation

Valueの検証には、Operation declarationのTypeExprを使用する。

  • Namedを参照先Typeへ解決する。
  • Required fieldまたはparameterの欠落を拒否する。
  • 未宣言のRecord fieldを拒否する。
  • Optional fieldの省略とUnitを区別する。
  • EnumおよびUnionの未知caseを拒否する。
  • Union discriminator、payloadの有無、payload typeをcase declarationに照合する。
  • Entryとして宣言されたStringがcanonical pathであることを検証する。
  • Integerの相互運用範囲とNumberの有限性を検証する。

Core request validation

Hostは、Transport Bindingがframing、media type、authentication等を受理した後にlogical requestを検証する。この検証は、interaction固有のObject / Interface解決より前に行う。

  1. Request structとして解釈できない、required fieldがない、未知fieldがある、Path等のfield semanticsに違反する場合はInvalidRequest。
  2. Field長、nesting、collection件数、observeのdepth等がHostのboundedな上限を超える場合はResourceLimitExceeded。
  3. 検証済みrequestについて、各interaction固有のtarget resolutionとpreconditionを評価する。

InvalidArgumentsを使用するのは、call_operation request自体はvalidであり、固定したOperation declarationにargumentsが適合しない場合だけである。Transport route、authentication、media type、framing等のfailureは、Core interactionのProtocol failureとして扱わない。

observe

observeは、一つのpathで現在公開されているObjectと、そこから指定depthまでのindexable childrenを、一つの観測として取得する。

struct ObserveRequest {
    path: String,
    depth: u32,
}

struct ObjectObservation {
    object: Object,
    children: Option<Vec<ObjectObservation>>,
}

type ObserveResponse = ObjectObservation;

起点Objectのdepthを0とする。通常のdepth = 0では対象Objectだけを観測し、root nodeのchildrenを省略する。

  • Root nodeのObjectはrequest pathが指すObjectである。Root pathが/の場合はObjectのnameが空文字列でなければならず、非root pathでは最後のsegmentとObjectのnameが一致しなければならない。
  • Request pathのObjectがその主体へ公開されていない場合はNotFoundを返す。
  • 各childのpathは、parent pathへchild Objectのnameを追加して導出する。
  • 同じparent nodeのchildrenは、互いに異なるnameを持つ。
  • Nodeのdepthがrequestのdepthより小さい場合、childrenは必須であり、その主体へ公開された直接のindexable childrenをすべて含む。Childが存在しない場合は空listとする。
  • Nodeのdepthがrequestのdepthと等しい場合、childrenを省略する。省略されたchildrenからemptyを推測してはならない。
  • Requested depthより深いnodeをresponseへ含めない。
  • その主体へ公開されていないObjectはresponseへ含めず、存在や省略を示すmarkerも返さない。

Success responseは、requested depthまで完全でなければならない。Node数によるpartial success、limit、truncatedは定義しない。Hostがresource budget内で完全なresponseを構築できなければ、ResourceLimitExceededを返す。

各nodeのObjectがvalidatorを持つ場合、Clientはrequest pathとchildrenのnameから導出したpathに対応付けてcacheし、条件付きOperationのtargetを構築できる。

HostとClientは、各nodeの配置pathに対してInterface scopeの祖先制約を検証する。Self-scopeを含み、root Objectではscope /を使用できる。制約に違反する参照を含むObject responseはInvalidResponseとし、そのObjectを操作に使用しない。

fetch_interface

fetch_interfaceは、Interface参照に対応する現在のInterface Descriptorを取得する。

Interface参照とvalidator

Object.interfacesの各要素、FetchInterfaceRequest.interface、FetchInterfaceResponse.interface、InterfaceTarget.referenceには、いずれもInterfaceReferenceを使用する。同じWorldspace context内の同じ有効な(scope, name)は、同じInterfaceを表す。ただし、Hostはvalidatorを使用してDescriptorを時間とともに変更できる。

FetchInterfaceResponse.validatorは、HostがInterface Descriptorの現在状態に付与するoptionalなopaque bytesである。Clientは内容を解釈しない。観測したDescriptorに基づいてOperationを実行するときは、Interface参照とともにInterfaceTargetへコピーする。

Interface参照とvalidatorには、同じ有効期間内で次の規則を適用する。有効期間はscope Objectとの紐付きに依存する。

  • 同じInterface参照と同じvalidator valueを持つ観測は、validatorが存在する場合、同じInterface Descriptorを表す。
  • 同じInterface参照と異なるvalidatorを持つ観測は、同じInterfaceの異なる状態を表す。
  • Validatorを返さないInterface参照は、その参照が有効である間、異なるDescriptorへ変更してはならない。
  • 同じInterface参照のDescriptorを変更可能にするHostはvalidatorを返し、Descriptorの変更時に以前と異なるvalidatorを発行しなければならない。
  • Validator valueはInterface参照と組み合わせて比較する。異なるInterface参照で同じopaque bytesを使用してもよい。
  • Validatorから状態の順序、新旧、Descriptor内容を推測してはならない。

生成方法はHostが自由に選べる。Generation、content hash、database revision等を使用してよい。Clientはその方法を解釈せず、Interface参照とvalidator valueの組をOperation executionのpreconditionとして使用する。

Interface validatorは、Client runtimeがInterface Descriptor observationとともに保持するprotocol metadataである。AIやユーザーへ公開するApplication dataではない。

Scope Objectの紐付き

Interface scopeは、そのpathのObjectに紐づく。Scope Objectがpathから消えると、そのObjectに紐づくInterface参照の有効期間が終了する。未公開のInterfaceへのfetch_interfaceはInterfaceNotFound、削除されたObject自身と消えた配下へのobserve / call_operationはNotFoundとなる。同じpathへ別Objectが公開された場合は、現在の公開状態で解決する。

FetchInterfaceResponse.scope_refは、scope Objectが同じ主体へ公開するObject.refである。Hostはそのrefがあれば同じstringを返し、なければfieldを省略する。別のscope専用identityやvalidatorを生成するfieldではない。

Scope Objectとの紐付き、Descriptor、Interface validatorは、一つの整合した観測として取得する。別々の時点の値を組み合わせず、Clientにscope Objectやその祖先の追加取得を必須にしない。

Clientはscope_refをInterface observationとともに保持し、その観測を使うcallでは、存在する値をInterfaceTarget.scope_refへコピーする。Hostは指定されたscope_refと、現在のscope ObjectのrefをUnicode code point sequenceの完全一致で比較する。一致しない場合、または現在のObjectがrefを持たない場合は、紐付きを確認できないためInterfaceMismatchとする。

この不一致は、異なるrefから別Objectだと断定するものではない。指定された同一性条件を満たせないことを表す。確認はClientの事前確認だけで済ませず、Hostがcallの他のpreconditionと一貫して行う。

scope_refは、Interface参照の識別子には含めない。識別とmembershipには引き続き(scope, name)を使用し、通常の表示も変更しない。Scope Objectの状態validatorは紐付きの確認に使わず、通常の状態更新で紐付きを無効にしない。Descriptorの変更検出には、独立したInterface validatorを使う。

Callでscope_refが省略された場合は、scope Objectのidentity preconditionを適用しない。Scope Objectのrefはoptionalであり、refなしではClientが同じpathのObjectの差し替えを追跡できないことも、仕様上許容される。

Hostは、refがないことやidentityを追跡できないことだけを理由に拒否せず、現在の公開状態と残るpreconditionに従う。Hostにref生成を要求せず、Clientにも疑似refやscope validatorの生成を要求しない。

Clientがscopeの削除・移動・差し替えによる有効期間終了を確認した場合は、旧Interface observationを新しいcallへ再利用しない。必要なら現在の参照を再取得し、同じpathへ再公開された参照の観測と、終了した有効期間のcacheを区別する。scope_refの相違や欠落によって以前の紐付きを確認できなくなった場合も、旧観測をそのまま付け替えない。異なる有効期間のDescriptorを、同じvalidatorであることだけから同一とみなさない。

一方、refも削除の観測もなければ、有効期間の切り替わりを検出できるとは限らない。Clientは得られた情報で通常のvalidationを行うが、検出不能な差し替えを防ぐ保証はない。この許容によって、存続中の同じInterfaceのDescriptor整合性が弱まることはない。

Scopeが別pathへ移動した場合は別参照として扱い、Clientは移動を追跡しない。実行開始済みcallが固定した観測と、結果検証用Descriptorは、有効期間終了後も保持する。

Requestとresponse

struct FetchInterfaceRequest {
    interface: InterfaceReference,
}

struct FetchInterfaceResponse {
    interface: InterfaceReference,
    scope_ref: Option<String>,
    descriptor: InterfaceDescriptor,
    validator: Option<Vec<u8>>,
}

Clientはresponseに対して、次の検証を行う。

  • Responseのinterfaceがrequestと一致する。
  • Descriptorが構造検証および意味検証に成功する。
  • DescriptorのformatがClientの対応するversionである。
  • 同じ有効期間のInterface参照と同じvalidator valueを持つ既存cache entryがある場合、validatorが存在する観測ではDescriptorがそのentryと一致する。Scopeの紐付きが確認できなくなった旧cacheとの比較は、前節の規則に従う。

ResponseやDescriptorがProtocol contractに違反した場合は、Hostが返したProtocol failureとはせず、Client-localなInvalidResponseとして扱う。Descriptorが構造的にはvalidでも、そのformatをClientが実装していない場合はUnsupportedDescriptorFormatとする。この二つのclassificationを混在させない。

Validatorが存在するInterface observationは、同じ有効期間内の(interface, validator)をkeyとしてcacheする。Clientはscope_refも観測metadataとして保持する。Interface参照ごとに最後に成功した観測を現在のcache entryとしてよいが、実行中のOperationが参照する観測は、responseのvalidationが完了するまで保持する。

同じ有効期間の観測には、次の規則を適用する。終了や紐付きの不一致を確認した旧cacheは、新しいcallには使用しない。

  • 同じInterface参照でvalidatorが異なる場合は、同じInterfaceの新しい状態としてlatest observationを更新する。
  • Validatorが存在しないInterface参照はimmutableなDescriptorとして、Interface参照だけをcache keyに使用できる。
  • 同じInterface参照と同じvalidator valueで異なるDescriptorを受け取った場合はInvalidResponseとして扱い、そのDescriptorをOperationへ使用しない。
  • 新しい観測を得ても、異なるvalidatorを使用して実行中のrequestに必要なDescriptorをresponse validation前に破棄しない。

fetch_interfaceは現在のDescriptorを観測するinteractionであり、requestにvalidatorを含めない。Responseにvalidatorがあれば、Clientは後続のcall_operationで、そのDescriptorに基づくOperation実行の必須preconditionとして使用する。Validatorがなければ、そのInterface参照のDescriptorはimmutableであり、call_operationのinterface.validatorを省略する。

Hostは、主体へ公開したObjectのinterfacesに含まれる各参照について、その主体がfetch_interfaceで現在のDescriptorを取得できるようにしなければならない。現在公開されていないInterface参照にはInterfaceNotFoundを返す。Descriptor取得の認可失敗としてPermissionDeniedを使用しない。

fetch_interfaceには完全な(scope, name)を指定するため、Clientに祖先Objectを順に取得するlookupは要求しない。Hostはscope Objectと、それに紐づくInterfaceの現在の公開状態を解決する。近い祖先の同名Interfaceへfallbackしない。

このrequestは、対象Objectのpathを持たない。特定Objectで利用できるかを判定する祖先制約(self-scopeを含む)は、observeとcall_operationで検証する。Scope外のObjectでは使えないことと、Interface単独の取得可否は同一視しない。

call_operation

call_operationは、指定pathにあるObjectへOperationを実行する。

struct Target {
    path: String,
    validator: Option<Vec<u8>>,
}

struct InterfaceTarget {
    reference: InterfaceReference,
    scope_ref: Option<String>,
    validator: Option<Vec<u8>>,
}

struct CallOperationRequest {
    target: Target,
    interface: InterfaceTarget,
    operation: String,
    arguments: BTreeMap<String, Value>,
}

struct CallOperationResponse {
    result: Value,
    validator: Option<Vec<u8>>,
}

target.pathには、Operationを実行するObjectのpathを指定する。target.validatorには、Clientが観測したObjectのvalidatorを条件として指定する。Objectがvalidatorを持たない場合や、無条件実行の場合は省略する。

interface.referenceには、ClientがObjectのinterfacesから選択し、Operation schemaを取得するために使ったInterface参照を指定する。interface.validatorは、その参照へのfetch_interfaceで観測したvalidatorである。Responseにvalidatorが存在する場合は必ず指定し、validatorが存在しないimmutableなInterface参照では省略する。

interface.scope_refは、そのDescriptor observationで得たscope Objectのoptionalなrefである。観測に存在する場合はコピーし、存在しない場合は省略する。Scope Objectを別途観測して得た新しいrefだけを、古いDescriptorへ付け替えてはならない。紐付きが確認できなくなった場合は、Interface observationを取得し直す。

argumentsは、Operation parameterに対応する名前付きrecordである。引数がない場合も、空のmapを送る。

Hostは、現在その主体へ公開しているObjectとInterfaceに基づいてOperationを解決する。複数のfailure条件が同時に成立する場合も、次の順で最初に該当するcodeだけを返す。

  1. target.pathのObjectが公開されていない場合はNotFound。
  2. 指定されたtarget.validatorが現在のObject validatorと一致しない場合はValidatorMismatch。
  3. interface.referenceが、現在そのObjectで公開されているObject.interfacesに(scope, name)の完全一致で含まれない、対象pathへのInterface scopeの祖先制約(self-scopeを含む)に違反する、または指定されたinterface.scope_refが現在のscope Objectのrefと一致しない場合はInterfaceMismatch。指定があるのに現在のrefがない場合も一致しない。scope_refの省略だけではこのfailureにしない。
  4. 現在のInterfaceがvalidatorを持つにもかかわらずinterface.validatorが省略されている場合はInterfaceValidatorRequired。
  5. 指定されたinterface.validatorが現在のInterface validatorと一致しない場合はInterfaceValidatorMismatch。現在のInterfaceがvalidatorを持たない場合にvalidatorが指定されたときも一致しないものとする。
  6. operationが現在のInterface Descriptorに宣言されていない場合はOperationNotFound。
  7. HostがObject validatorを要求するOperationにtarget.validatorが指定されていない場合はValidatorRequired。
  8. argumentsが現在のOperation declarationに適合しない場合はInvalidArguments。
  9. 宣言されたOperationの存在を公開しつつ、その主体による実行を認めない場合はPermissionDenied。

指定されたObject validatorやInterface validatorの不一致は、現在のDescriptorに基づくOperation lookupとarguments validationより先に判定する。古いObjectやschemaに基づくrequestをOperationNotFoundやInvalidArgumentsとして扱わず、Clientが対応する観測を更新できるようにするためである。

Hostは一つのlinearization pointで、現在のObject identityと状態、Object validator、Objectが公開するInterface参照、scope Objectとの紐付きと指定されたscope_ref、Interface validator、Interface Descriptor、Operation implementation、実行対象を解決する。同じ時点で各preconditionを確認し、そのcallが使用する観測を固定する。

その後にObjectやInterfaceが変化しても、callの途中で別のscope Object、Descriptor、Operation implementation、実行対象へ解決し直してはならない。Argumentsとresultのvalidationには、固定したInterface Descriptorの同じOperation declarationを使用する。Authorizationも、固定したObject、Interface、主体のcontextに対して評価する。

Hostがtarget.validatorをbackendへのcommit条件として使用する場合は、validatorの比較とcommitをatomicに行う。Responseのvalidatorは、Operation完了後にHostがそのObjectについて発行する現在のvalidatorであり、Object validatorを提供しない場合は省略する。Validatorの有無や値から、Operationが何を行ったか、Application dataを変更したかを推測しない。

Validatorはopaque bytesの完全一致で比較する。Clientは生成方法や内容に基づいて分岐してはならない。

Hostは、OperationをInterfaceから除外して存在を非公開にしてもよい。Interfaceに含めたうえで、実行時にPermissionDeniedとして拒否することもできる。

Clientはresultを、requestのinterfaceが指定し、Hostがprecondition確認時に固定したOperation return typeに従って検証する。Success responseがvalidationに失敗した場合は、Hostから返されたProtocol failureとはせず、InvalidResponseとして扱う。そのvalueをApplicationへ渡さない。

Host Runtimeがimplementation resultのencodeやvalidationのfailureを送信前に検出した場合は、effectのcommit状況に応じてerrorを返す。Operationのeffectがcommitされていないと保証できる場合はInternalとする。未実行またはrollback済みであることを保証できず、validなsuccess responseも返せない場合はOperationOutcomeUnknownとする。

Worldspace composition

Clientが扱う絶対pathとInterface scopeは、常に接続先Hostが公開するClient-facing Worldspaceのrootを基準にする。Proxyや合成によってUpstream Worldspaceの公開位置が変わる場合は、Hostが公開先とUpstreamの座標を対応付ける。Upstreamのrootやmount構成を、Clientに追跡・補完させない。

Upstreamのrootを/githubへ配置する場合、座標は次のように対応する。

項目 Upstream 公開先
Root / /github
Object path /my-rep/issues /github/my-rep/issues
Interface参照 { scope: "/", name: "issue.read" } { scope: "/github", name: "issue.read" }
Repository scope /my-rep /github/my-rep
entry /my-rep/issues/123 /github/my-rep/issues/123

別のUpstreamを/gitlabへ配置した場合、その{ scope: "/", name: "issue.read" }は{ scope: "/gitlab", name: "issue.read" }となる。同じlocal nameでも、scopeによって区別する。さらに全体を/servicesへ配置するHostは、/githubを/services/githubへ変換する。

一方、Worldspaceの配置を変えないHTTP proxyは、scopeを変更する必要がない。HTTP endpointのpath prefixとWorldspace pathは混同しない。

座標の変換

合成Hostは、次の対応を一貫させる。

  • observeのrequest pathをUpstreamへ変換する。ResponseのObjectのnameとchildrenは公開先pathの規則を満たすようにする。特にUpstream rootの空のnameを、非rootのmount先へそのまま公開しない。
  • Object.interfacesの各構造化参照のscopeを公開先へ変換し、公開先での祖先制約と名前の一意性を保つ。
  • fetch_interfaceを適切なUpstreamと構造化参照へ対応付ける。ResponseのinterfaceはClientが要求した公開先の参照にする。
  • call_operationのtarget pathとInterface参照をUpstreamへ対応付ける。Local name単独で転送先を選ばない。
  • scope_refは公開先scope Objectのrefと一致させる。Upstreamのscope Objectが持つrefとの対応をHostが保持し、公開先で指定されたpreconditionを検証してからUpstreamへ対応付ける。Opaqueなrefへpath prefixを機械的に加えたり、指定された条件を転送時に落としたりしない。公開先scope Objectがrefを持たない形態も許可する。
  • Descriptorでentryと宣言されたargumentsをUpstreamへ、resultを公開先へ変換する。Named type、record、list、unionの内部にあるentryも同じ規則で扱う。

通常のstring、opaqueなjson、documentation内のpathらしい文字列は、推測して書き換えない。Application固有の別の座標系は、Application adapterが扱う。

公開subtree外のscopeやentryに対応付けがない場合、Upstream pathをそのまま公開したり、scopeを削除したりrootへ付け替えたりしてはならない。部分合成の対応付け方針と詳細な拒否条件は未確定だが、失敗時にも既存のProtocol failure契約を守る。

特に、Upstreamでeffectがcommit済みのOperationについてresultを変換できない場合は、未実行を保証するerrorへ置き換えない。Validなsuccessを返せず、未実行またはrollbackを保証できない場合はOperationOutcomeUnknownとして扱う。

同一性と観測整合性

  • 公開先の同じ有効な(scope, name)とvalidatorを、異なるDescriptorへ対応付けない。Validatorのない参照のDescriptorは、その参照が有効な間immutableとする。
  • 異なるUpstreamのObjectへ同じ有効な公開refを割り当てない。
  • Routingやmountの変更後に、古いObject validatorで別の対象へOperationを実行できないようにする。
  • Upstream validatorをそのまま使用するか、Host側で対応付け・wrapするかは、これらの保証を満たす範囲でHostが決める。
  • Scopeが変わればProtocol上は別のInterface参照である。Clientに移動前後の同一性を推測させない。

同じUpstream Objectを複数のscope配下へ配置すると、公開するInterface集合は配置ごとに変わり得る。このため、異なる公開representationへ同じrefとvalidatorを無条件には引き継げない。共通祖先scopeでの共有や、projectionごとのidentity割当等の選択は、未解決事項に残る。

Renameやmount変更では、新しいscopeを別参照として扱う。同じpathでscope Objectが差し替わる場合は、optionalなscope_refの確認を適用する。Clientによる移動追跡は要求しない。これらの扱いによって、既存のvalidatorと実行中callの一貫性を弱めない。

Protocol responseとfailure

Core interactionの結果は、成功responseまたはProtocol failureである。

enum ProtocolResult<T> {
    Success(T),
    Failure(ProtocolError),
}

struct ProtocolError {
    code: ProtocolErrorCode,
    message: String,
}

enum ProtocolErrorCode {
    InvalidRequest,
    NotFound,
    InterfaceNotFound,
    InterfaceMismatch,
    OperationNotFound,
    InvalidArguments,
    ValidatorRequired,
    ValidatorMismatch,
    InterfaceValidatorRequired,
    InterfaceValidatorMismatch,
    PermissionDenied,
    ResourceLimitExceeded,
    Internal,
    OperationOutcomeUnknown,
}

Clientが分岐に使用するのはcodeである。messageは人間向けのboundedな診断であり、分岐用の安定した値ではない。Hostは秘密情報、非公開Object、backend path、credential、内部stack traceをmessageへ含めない。

返してよいcodeは、interactionごとに次のものへ限定する。

Code observe fetch_interface call_operation
InvalidRequest ✓ ✓ ✓
ResourceLimitExceeded ✓ ✓ ✓
NotFound ✓ — ✓
InterfaceNotFound — ✓ —
InterfaceMismatch — — ✓
InterfaceValidatorRequired — — ✓
InterfaceValidatorMismatch — — ✓
OperationNotFound — — ✓
ValidatorRequired — — ✓
ValidatorMismatch — — ✓
InvalidArguments — — ✓
PermissionDenied — — ✓
Internal ✓ ✓ ✓
OperationOutcomeUnknown — — ✓

Hostがそのinteractionに許可されていないcodeを返した場合、Clientはerror responseをInvalidResponseとして扱う。

Validatorと観測に関するerrorの回復対象は、次のとおりである。

  • NotFoundでは、Clientは対象Entryをstaleとして扱い、必要に応じて親を再観測する。
  • InterfaceNotFoundでは、ClientはそのInterface observationを使用せず、参照元Objectを再観測できる。
  • ValidatorRequiredまたはValidatorMismatchでは、Clientは対象Objectを再観測する。
  • InterfaceMismatchでは、Clientは対象Objectを再観測し、現在公開されているInterface参照の集合を取得する。Scopeの紐付きが変わった可能性もあるため、使用したInterface observationを新しいcallへ再利用せず、必要ならfetch_interfaceで再取得する。
  • InterfaceValidatorRequiredまたはInterfaceValidatorMismatchでは、Clientは同じInterface参照をfetch_interfaceで再観測し、現在のDescriptorに基づいてOperationを再構築する。

同じOperationを再試行する場合は、先に新しい観測を取得しなければならない。

  • InvalidRequestまたはInvalidArgumentsでは、Clientは同じrequestを変更せずに再送しない。
  • ResourceLimitExceededでは、Clientはlogical request shapeやobservation depth等を削減できるが、call_operationを自動再実行しない。

call_operationがOperationOutcomeUnknown以外のProtocol failureを返した場合、HostはOperationのeffectがcommitされていないことを保証する。OperationOutcomeUnknownを返すのは、未実行またはrollbackを保証できず、validなsuccess responseも返せない場合である。

OperationOutcomeUnknownを受けたClientは、対象Objectをstaleとして扱い、Operationを自動再実行しない。再観測またはdomain固有の照会手段を、利用者へ提示する。

InternalはHost内部のfailureであり、call_operationではeffectがcommitされていないと保証できる場合だけ返す。ClientはInternalを理由にOperationを自動再実行しない。Transport timeoutやconnection lossでresponse自体を受け取れない場合も、Operation outcomeは不明になり得る。ただし、これはProtocol failureではなくTransport failureとして扱う。

Objectを主体へ公開しない場合、HostはそのObjectをClient-facing Worldspaceに存在しないものとして扱う。observeとcall_operationにはNotFoundを返し、存在を秘匿する目的でPermissionDeniedを返してはならない。

PermissionDeniedは、現在公開されているInterfaceに宣言されたOperationの存在を公開しつつ、実行を認めない場合に使用する。Operation自体をInterfaceから除外している場合は、OperationNotFoundを返す。Operation argumentが参照するdomain resourceの不存在やbusiness rule failureを、対象ObjectのNotFoundへ変換しない。

InvalidResponseとUnsupportedDescriptorFormatは、いずれもClient-localなclassificationであり、ProtocolErrorCodeには含めない。Success responseまたはerror responseがProtocol contractに違反した場合は前者、validなDescriptor formatをClientが実装していない場合は後者を使用する。

Protocol failureとdomain failureは混在させない。現在のProtocolでは、domain failureをApplicationのOperation resultに含まれるUnion caseとして明示する。専用のfailure envelopeは定義しない。

Transport Bindingは、Protocol Resultをstatus code、response envelope、stream termination等へ対応付ける。Authentication、route、media type、framing、response不達等のTransport failureを、Protocol errorへ変換しない。

Resource limit

HostとClientは、untrusted inputによるresource exhaustionを避けるため、少なくとも次の対象へ上限を設ける。

  • Pathとして解釈するstring、validator、name、description、documentationの長さ。
  • Interface Descriptor内のdeclaration、documentation、field等の総量。
  • ObjectごとのInterface参照数。
  • Type declaration、Operation、parameter、field、caseの数。
  • TypeExprとValueのnesting depth。
  • Record field数とList要素数。
  • observeのdepthとresponse node数。
  • Protocol error messageの長さ。

各実装は、これらにboundedな上限を設定する。Request fieldだけで判定できる上限違反は、interaction固有のtarget resolutionやOperation dispatchより前に、ResourceLimitExceededとして拒否する。

Hostが取得responseを構築する途中で、response node数やlogical collection size等の上限超過を検出した場合も、success responseの代わりにResourceLimitExceededを返してよい。

call_operationでは、dispatch後にresultのlogical size、validation、backend budget等の問題を検出した場合、effectがcommitされていないと保証できるときだけResourceLimitExceededまたはInternalを返す。未実行またはrollbackを保証できない場合はOperationOutcomeUnknownとする。

Transport implementationやdeploymentのmessage-size上限、response不達等は、Protocol failureへ変換しない。これらはTransport failureとして扱う。

Transport Bindingの要件

Transport Bindingは、これらのlogical structに対して次を定義する。

  • Requestとresponseのframing。
  • Protocol primitiveのconcrete encoding。
  • Object、Descriptor、Valueのencoding。
  • Optional fieldの省略。
  • Protocol failureのmapping。
  • Object validator、Interface validator、Operation preconditionのmapping。

Transport固有のmessage size、timeout、concurrency、security、connection policyは、Transport実装とdeploymentの責務である。BindingがWIP Protocolとして値やnegotiationを定義する必要はない。複数のinteractionを並行実行またはmultiplexする場合も、それぞれは独立したrequestとresponseを持つ。

Transport Bindingは、logical typeの区別やvalidation semanticsを失ってはならない。