| English | 中文版 |
Spec Number: NPS-2 Status: Proposed Version: 0.22 Date: 2026-08-31 Port: 17433 (default, shared) / 17434 (optional dedicated) Authors: Ori Lynn / INNO LOTUS PTY LTD Depends-On: NPS-1 (NCP v0.12), NPS-3 (NIP v0.15), NPS-4 (NDP v0.13)
This document is the NWP detailed specification. For a suite overview see NPS-0-Overview.md.
Keywords “MUST”, “MUST NOT”, “REQUIRED”, “SHOULD”, “MAY” in this document are interpreted per RFC 2119.
NWP defines the AI-native request/response semantics for interacting with Neural Nodes. Agents use nwp:// addresses to query data, invoke operations, subscribe to changes, route through cluster anchors, and bridge to external protocols across the Memory / Action / Complex / Anchor / Bridge roles. Responses are directly machine-understandable, requiring no semantic parsing layer.
NWP is a semantic protocol layer, not a Memory-Node-only REST API. It can be carried over native NCP sessions or over the HTTP overlay mode defined in §2.2; REST is only an analogy for familiar request/response semantics or an external protocol target of Bridge Nodes.
LLM-serving is expressed as an NWM LLM/Thinking Profile (§4.2a) on ordinary Action or Complex Nodes, not as a sixth base node_type. This keeps node_type tied to protocol responsibility while still giving model-serving nodes a standard discovery shape.
| Type | Responsibility | Typical Data Sources |
|---|---|---|
| Memory Node | Data storage and retrieval, no compute logic | RDS, NoSQL, file systems, vector databases |
| Action Node | Executes operations, returns results or side effects | Functions, external APIs, message queues, Webhooks |
| Complex Node | Mixed data and operations, with sub-node references | All of the above + sub-node references |
| Anchor Node | Cluster control plane and external entry point — routes inbound NWP Action/Query frames to member nodes via NOP, optionally maintains member-node topology |
AaaS platforms, multi-agent service gateways, sub-cluster routers |
| Bridge Node | Translates between NPS frames and non-NPS protocols (HTTP/HTTPS, gRPC, MCP, A2A) | Calls to legacy REST APIs, gRPC services, Model Context Protocol servers, Agent-to-Agent endpoints |
A node MAY simultaneously carry more than one role (for example, a single deployment can be both a Memory Node and an Anchor Node when role separation is unnecessary). Multi-role declaration travels in the NDP Announce frame’s node_roles field (NPS-4 §3.1).
Thinking Node is a product-facing alias, not a wire-level node type. A node that serves LLM completions SHOULD declare
node_type: "action"when it only performs model actions, ornode_type: "complex"when it also owns memory, tools, routing, or graph behavior. It advertises the standardprofiles.llmNWM block (§4.2a) and the appropriate NIP capabilities (llm:*, NPS-3 §5.1).
Anchor Node and Bridge Node were introduced together by NPS-CR-0001, replacing the original
Gateway Nodetype:
- Anchor Node inherits the cluster-entry / NOP-routing role that Gateway Node was carrying. It is stateless per request but MAY maintain a long-lived registry of member nodes.
- Bridge Node is a new type whose role is NPS ↔ non-NPS protocol translation, in both directions. It is stateless per request and does not participate in cluster topology. Direction is declared per protocol in NDP
Announce(bridge_protocolsfor outbound,bridge_inbound_protocolsfor inbound). (NPS-CR-0010 settled this: alpha.3–alpha.15 shipped a normative “outbound-only” narrowing that existed solely to keep the nameBridgedistinct from the then-separatecompat/*-ingresspackages; those packages are now absorbed into the Bridge package, and the restriction is lifted.)- The original
Gateway Nodeterm is retired; the wire value"gateway"is removed and parsers MUST reject it with a clear error referencing CR-0001.
Gateway Node (removed in v1.0-alpha.3) — Split into Anchor Node (cluster entry / NOP routing) and Bridge Node (NPS↔non-NPS protocol translation). See NPS-CR-0001 for full rationale and migration notes. Implementations MUST reject the legacy
node_type: "gateway"withNWP-MANIFEST-NODE-TYPE-REMOVEDand the legacynode_roles: ["gateway"](or legacynode_kind: "gateway") withNDP-ANNOUNCE-ROLE-REMOVED; response SHOULD include ahintpointing to NPS-CR-0001.
Two fields participate in node-role declaration at different protocol layers. They are intentionally distinct in name and semantics:
| Field | Protocol | Layer | Cardinality | Authoritative for |
|---|---|---|---|---|
node_roles |
NDP Announce (NPS-4 §3.1) |
Discovery | Array — all roles the node carries | Full role set; used for discovery filtering and cluster membership |
node_type |
NWP NWM (§4.1) | Service | String — single operative role | Which role this particular /.nwm endpoint is serving |
Constraint: node_type MUST be one of the values declared in the node’s node_roles. Validators SHOULD verify this against cached NDP Announce data when available.
Multi-role nodes (e.g., node_roles: ["anchor", "memory"]) MAY expose separate /.nwm endpoints on different paths or ports, each advertising a different node_type; or may choose a single dominant node_type matching the primary role served at that endpoint. In either case the constraint holds: node_type ∈ node_roles.
An Anchor Node MUST:
Action and Query frames addressed to the cluster (i.e. to the Anchor’s NID rather than to a specific member NID).ActionFrame into a NOP TaskFrame and delegates to a local NOP orchestrator (see NPS-AaaS-Profile §2).activation_mode per NPS-Node Profile §6). Member nodes register on cluster join via NDP Announce frames carrying cluster_anchor referencing the Anchor Node’s NID; deregistration follows standard NDP offline semantics.A cluster MUST have at least one Anchor Node. High-availability deployments MAY operate multiple Anchor Nodes for the same cluster; the consensus protocol between Anchor Nodes is implementation-defined and is deferred to NPS-AaaS Profile L3.
Anchor Nodes that maintain a member registry MUST expose it via the reserved query types topology.snapshot and topology.stream (§12), per NPS-CR-0002. Both are mandatory at NPS-AaaS Profile L2 and above.
A Bridge Node translates in both directions between NPS frames and non-NPS protocols. It MUST implement at least one direction, and MUST declare which protocols it serves in which direction via NDP Announce (NPS-4 §3.1): bridge_protocols for outbound, bridge_inbound_protocols for inbound. A Bridge Node MUST NOT be assumed to serve a direction it did not declare. (NPS-CR-0010)
Outbound — NPS → external protocol. A Bridge Node serving outbound MUST:
bridge_target parameter that identifies the external protocol and endpoint. The canonical bridge_target wire shape is { "protocol", "endpoint", "extras"? }: protocol (string, required — one of "http", "grpc", "mcp", "a2a"); endpoint (string URL, required); extras (object, optional — per-protocol knobs such as HTTP method, headers, MCP tool, or gRPC call metadata). HTTP headers MUST travel inside bridge_target.extras.headers, not as a top-level bridge_target.headers field. Third-party adapters MAY add fields inside extras; unknown top-level fields and unknown extras members MUST be ignored by consumers.CapsFrame).Inbound — external protocol → NPS. A Bridge Node serving inbound MUST:
bridge_inbound_protocols, speaking that protocol’s native wire format.Query for reads, Action / Complex Node Invoke for calls — and translate the NWP response back into the foreign protocol’s response format.For MCP inbound, a conformant Bridge Node MUST serve initialize, ping, tools/list, tools/call, resources/list, and resources/read. Memory Nodes are projected as MCP resources; Action / Complex Nodes are projected as MCP tools.
A deployment MAY run inbound translation as a plain hosting library in front of NPS nodes, with no NID and no Announce. Such a deployment is not a Bridge Node — it is the Bridge library, and the MUSTs above do not bind it. Only a deployment that announces node_roles: ["bridge"] is a Bridge Node. (NPS-CR-0010 §3.3)
Bridge Nodes are stateless per request and do not participate in cluster topology, in both directions. A single Bridge Node MAY serve several protocols and both directions; deployments MAY operate dedicated Bridge Nodes per protocol or per direction for isolation.
Standard external protocols expected to be supported by reference Bridge Node implementations:
Additional protocol adapters MAY be registered through future CRs. The set of supported protocols is declared in NDP Announce.bridge_protocols / Announce.bridge_inbound_protocols (NPS-4 §3.1).
Attach a NWP interface to an existing HTTP service. The server distinguishes visitors by request headers:
Request contains X-NWP-Agent or HelloFrame → return application/nwp-*
Regular browser request (no above markers) → return text/html (normal website)
In Overlay mode, NWP uses HTTP transport with frames serialized in the HTTP body. See NPS-1-NCP.md §2.2.
nwp-url = "nwp://" host [":" port] "/" node-path ["/" sub-path]
host = <RFC 3986 host>
port = 1*DIGIT ; default 17433
node-path = segment *("/" segment)
sub-path = "query" / "stream" / "invoke" / "subscribe" / "actions"
/ ".schema" / ".nwm"
segment = 1*(ALPHA / DIGIT / "-" / "_")
| Sub-Path | Method | Node Types | Description |
|---|---|---|---|
/query |
POST | Memory | Single structured query (returns CapsFrame) |
/stream |
POST | Memory | Streaming query (returns StreamFrame sequence) |
/invoke |
POST | Action / Complex | Operation invocation endpoint |
/subscribe |
POST | Memory | Change subscription endpoint (HTTP mode, SSE) |
/actions |
GET | Action / Complex | List callable operations (returns NWM actions subset JSON) |
/.schema |
GET | All | Schema definition (returns AnchorFrame JSON) |
/.nwm |
GET | All | Full node manifest (returns NWM JSON) |
Every node MUST expose a machine-readable manifest at /.nwm, MIME type: application/nwp-manifest+json.
| Field | Type | Required | Description |
|---|---|---|---|
nwp |
string | Required | NWP version, currently "0.4" |
node_id |
string | Required | Node NID, format: urn:nps:node:{host}:{path} |
node_type |
string | Required | "memory" / "action" / "complex" / "anchor" / "bridge". The legacy value "gateway" was removed in v1.0-alpha.3 (see §2.1 Removed types and NPS-CR-0001); parsers MUST reject it with NWP-MANIFEST-NODE-TYPE-REMOVED. Any other unrecognized value MUST be rejected with NWP-MANIFEST-NODE-TYPE-UNKNOWN. This field declares the operative role this NWP service endpoint is serving — it MUST be one of the values in the node’s NDP Announce.node_roles (NPS-4 §3.1). For multi-role nodes, node_type selects which role is active at this endpoint; see §2.1 Node Role Resolution for the full constraint. |
display_name |
string | Optional | Human-readable node name |
manifest_version |
uint32 | Optional | Monotonically incrementing manifest version counter (starts at 1, incremented on every structural change). Servers MUST include X-NWM-Version: <manifest_version> on every GET /.nwm response. Agents SHOULD send If-None-Match: <manifest_version> on subsequent requests; unchanged manifests return 304 Not Modified. (NWP v0.14) |
manifest_updated_at |
string | Optional | ISO 8601 timestamp of the last manifest change, e.g. "2026-06-03T12:00:00Z". SHOULD be set whenever manifest_version is incremented. (NWP v0.14) |
min_agent_version |
string | Optional | Minimum NPS version the Agent must support, format "major.minor"; Agents below this version MUST be rejected with NWP-MANIFEST-VERSION-UNSUPPORTED |
min_assurance_level |
string | Optional | One of "anonymous" (default) / "attested" / "verified" (see NIP §5.1.1). Requests presenting a level lower than this MUST be rejected with NWP-AUTH-ASSURANCE-TOO-LOW (NPS-AUTH-FORBIDDEN); response SHOULD include a hint pointing to a CA enrolment URL. Default "anonymous" preserves backward compatibility with v1.0-alpha.2 nodes. Per-action override is permitted via the min_assurance_level field on an individual ActionSpec (§4.6). (NPS-RFC-0003) |
wire_formats |
array | Required | Supported encoding formats: ["ncp-capsule", "msgpack", "json"] |
preferred_format |
string | Required | Preferred format |
schema_anchors |
object | Optional | Pre-declared schema anchors, {name: anchor_id} |
capabilities |
object | Required | Node capability declarations, see §4.2 |
data_sources |
array | Optional | Underlying data source identifier list |
auth |
object | Required | Authentication requirements, see §4.3 |
rate_limits |
object | Optional | Rate limit declarations, see §4.4 |
actions |
object | Conditionally Required | MUST be populated for Action/Complex nodes; operation registry, see §4.6 |
endpoints |
object | Required | URLs for each functional endpoint |
graph |
object | Optional | Sub-node references (Complex Node only), see §11 |
tokenizer_support |
array | Optional | List of tokenizers supported by the node (see token-budget.md) |
stability |
string | Optional | Lifecycle stage: "experimental" / "stable" / "deprecated". Marketplace / NeuronHub discovery clients use this to filter or warn on non-stable services. Default: "stable" (backward-compatible — pre-0.11 manifests are treated as stable). Per-action override permitted via ActionSpec.stability (§4.6). |
sla |
object | Optional | SLO commitments for the node, see §4.7. Advisory only; the protocol does not enforce these. Per-action override permitted via ActionSpec.sla (§4.6). |
billing |
object | Optional | Commercial metadata for the node (metering profile + price hint), see §4.8. Advisory only; the protocol does not collect or settle charges. Per-action override permitted via ActionSpec.billing (§4.6). |
subscription_policy |
object | Optional | Renewable-subscription limits: default_lease_seconds, max_lease_seconds, and renew_before_seconds; see §13.4. Absence means the v0.21 unleased compatibility behavior. |
trust_anchors |
array of strings | Optional | NIDs of CA nodes the Anchor accepts as IdentFrame issuers (e.g. ["urn:nps:agent:ca.example.com:root"]). Consumers SHOULD use this to pre-validate their issuer before connecting. When absent, the node accepts any CA trusted by the NIP verification chain. |
profiles |
object | Optional | Structured protocol profiles layered on top of the base node role, see §4.2a. Unknown profile keys MUST be ignored by consumers. |
| Capability Key | Type | Description |
|---|---|---|
query |
bool | Supports QueryFrame (single query) |
stream_query |
bool | Supports streaming queries (StreamFrame response) |
aggregate |
bool | Supports aggregation queries (QueryFrame.aggregate) |
subscribe |
bool | Supports change subscriptions (DiffFrame push) |
subscribe_filter |
bool | Supports subscriptions with filter conditions |
vector_search |
bool | Supports vector similarity search |
token_budget_hint |
bool | Supports trimming responses based on CGN budget |
ext_frame |
bool | Supports extended frame header (large frame mode) |
e2e_enc |
bool | Supports NCP E2E encryption (ENC=1, see NPS-1-NCP §7.4) |
inline_anchor |
bool | Supports returning updated AnchorFrame inline in responses |
profiles is an optional object for structured capability profiles that refine a node’s base role. Profiles do not introduce new node_type values. A consumer that does not understand a profile key MUST ignore that key and continue using the base NWP role, action registry, and capability flags.
profiles.llm)A node with profiles.llm is an LLM-capable Action or Complex Node. Product documentation MAY call it a “Thinking Node”, but the canonical NWM node_type remains "action" or "complex":
"action" when the endpoint only runs model actions and returns results."complex" when the endpoint also owns memory, tool orchestration, graph traversal, session state, or other non-trivial composition.llm:complete in its NDP/NIP capabilities and MUST implement the llm.complete ActionFrame contract (§7.5) when profiles.llm.actions contains "llm.complete".supports_stream = true, the node SHOULD also advertise llm:stream; if supports_tools = true, it SHOULD advertise llm:tool_call.context.supported = true MUST also advertise llm:context, list the implemented lifecycle actions, and implement §7.6 without silent stateless fallback.profiles.llm fields
| Field | Type | Required | Description |
|---|---|---|---|
profile_version |
string | Optional | LLM profile schema version. "0.1" is the stateless profile; "0.2" adds the optional stateful-context descriptor below. |
actions |
array[string] | Optional | Standard LLM action ids implemented by this node. Default: ["llm.complete"] |
provider |
string | Optional | Provider/runtime family, e.g. "willow", "ollama", "openai-compatible". Advisory only |
default_model |
string | Optional | Model id used when a request omits provider-specific routing hints |
models |
array | Optional | Model descriptors, see below |
supports_stream |
bool | Optional | Whether llm.complete accepts stream=true and returns a StreamFrame sequence |
supports_tools |
bool | Optional | Whether tool definitions and tool-call responses are supported |
supports_json_mode |
bool | Optional | Whether structured JSON/object completions are supported |
supports_embeddings |
bool | Optional | Whether embedding actions such as llm.embed are supported |
supports_rerank |
bool | Optional | Whether rerank actions such as llm.rerank are supported |
reasoning_visibility |
string | Optional | "none" / "summary" / "trace". trace exposes provider reasoning artifacts and is deployment-sensitive |
privacy |
object | Optional | Operator privacy policy hints, see below |
context |
object | Optional | Stateful LLM context support and operational limits; requires profile_version = "0.2" and §7.6 |
Model descriptor
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | Required | Model id accepted by LlmCompleteActionRequest.model |
display_name |
string | Optional | Human-readable model name |
modalities |
array[string] | Optional | Supported modalities, e.g. ["text"] or ["text","image"] |
context_window |
uint32 | Optional | Maximum input context window in native model tokens |
max_output_tokens |
uint32 | Optional | Maximum output tokens permitted by this node |
tokenizer |
string | Optional | Tokenizer id used for estimates and CGN hints |
cgn_profile |
string | Optional | CGN conversion profile id from cgn-profiles.yaml, when known |
Privacy descriptor
| Field | Type | Description |
|---|---|---|
retention |
string | Prompt/response retention policy, e.g. "none" / "session" / "30d" |
training |
bool | Whether prompt/response content may be used for model training |
region |
string | Optional processing or storage region hint |
Stateful context descriptor (profiles.llm.context)
| Field | Type | Required | Description |
|---|---|---|---|
supported |
bool | Required | Whether this node implements the §7.6 contract. false MUST NOT be inferred as partial support. |
operations |
array[string] | Required when supported | Implemented operations from create, append, fork, reset, release. |
persistence |
string | Required when supported | connection, process, or durable, with the lifecycle guarantees in §7.6.5. |
max_contexts_per_principal |
uint32 | Required when supported | Maximum live contexts owned by one authenticated principal/security scope. |
max_ttl_seconds |
uint32 | Required when supported | Maximum accepted idle TTL. |
tombstone_seconds |
uint32 | Required when supported | Minimum released/expired tombstone visibility promised by the node. |
Example
{
"node_type": "action",
"capabilities": { "query": false, "stream_query": false, "token_budget_hint": true },
"actions": {
"llm.complete": {
"description": "Complete a chat conversation",
"params_anchor": "nps:system:llm.complete:request",
"result_anchor": "nps:system:llm.complete:response",
"async": true,
"required_capability": "llm:complete"
},
"llm.context.status": {
"description": "Inspect an LLM context or recover an idempotent create outcome",
"params_anchor": "nps:system:llm.context.status:request",
"result_anchor": "nps:system:llm.context.status:response",
"async": false,
"required_capability": "llm:context"
},
"llm.context.release": {
"description": "Release an LLM context",
"params_anchor": "nps:system:llm.context.release:request",
"result_anchor": "nps:system:llm.context.release:response",
"async": false,
"required_capability": "llm:context"
}
},
"profiles": {
"llm": {
"profile_version": "0.2",
"provider": "willow",
"default_model": "willow-small",
"actions": ["llm.complete", "llm.context.status", "llm.context.release"],
"supports_stream": true,
"supports_tools": true,
"reasoning_visibility": "summary",
"context": {
"supported": true,
"operations": ["create", "append", "fork", "reset", "release"],
"persistence": "process",
"max_contexts_per_principal": 32,
"max_ttl_seconds": 3600,
"tombstone_seconds": 86400
},
"models": [
{
"id": "willow-small",
"modalities": ["text"],
"context_window": 128000,
"max_output_tokens": 8192,
"tokenizer": "cl100k_base",
"cgn_profile": "oa.reasoning"
}
],
"privacy": { "retention": "none", "training": false, "region": "us" }
}
}
}
| Field | Type | Required | Description |
|---|---|---|---|
required |
bool | Required | Whether authentication is required |
identity_type |
string | Conditionally Required | "nip-cert" / "bearer" / "none" |
trusted_issuers |
array | Conditionally Required | List of trusted CA URLs (required when identity_type is nip-cert) |
required_capabilities |
array | Optional | Capabilities the Agent MUST hold, e.g. ["nwp:query"] |
scope_check |
string | Optional | Scope validation mode: "prefix" (default) / "exact" |
ocsp_url |
string | Optional | OCSP validation endpoint |
| Field | Type | Description |
|---|---|---|
requests_per_minute |
uint32 | Max requests per Agent per minute |
requests_per_day |
uint32 | Max requests per Agent per day |
max_concurrent_streams |
uint32 | Max concurrent streams per Agent |
max_subscriptions |
uint32 | Max concurrent subscriptions per Agent |
Advisory SLO commitments. Clients (especially marketplace / NeuronHub aggregators) display or filter on these values; the protocol does not police them. All sub-fields are Optional.
| Field | Type | Description |
|---|---|---|
p95_latency_ms |
uint32 | Self-declared 95th-percentile end-to-end latency in milliseconds, measured at the node’s /.nwm reference endpoint |
availability |
string | Self-declared availability target as a decimal-fraction string, e.g. "0.999" for “three nines”. Format: 0\.[0-9]+; clients SHOULD interpret missing as “best effort”. |
sla_tier |
string | Optional named tier for marketplace listing: "best-effort" / "standard" / "premium". Free-form strings outside this enum are reserved for future extension and SHOULD be ignored by current clients. |
Advisory commercial metadata. The protocol does not authorize, meter, or settle charges; this field exists so marketplace listings, agent autoscalers, and budget gates can make informed decisions before invoking. All sub-fields are Optional.
| Field | Type | Description |
|---|---|---|
metering_profile |
string | Identifier of the metering model: "free" / "metered" / "flat-rate". Free-form values outside this enum are reserved and SHOULD be treated as "metered" by conservative clients. |
billing_unit |
string | Unit string when metering_profile = "metered", e.g. "per-token" / "per-request" / "per-cgn" / "per-second". |
price_hint |
string | Indicative price in ISO-4217 currency-prefixed decimal form, e.g. "USD 0.0002" per billing_unit. Hint only — the operator’s external contract is authoritative. |
currency |
string | ISO-4217 currency code (e.g. "USD", "EUR", "CNY"). Optional convenience field; price_hint already encodes the currency prefix. |
The requirement IDs below are exercised by
alpha19_hardening_vectors.json:
experimental,
stable, or deprecated. Absence normalizes to stable. Consumers preserve
an unknown raw value but normalize it to experimental for filtering and
MUST NOT rank it as stable.p95_latency_ms, when present, is a positive uint32;
availability is a decimal fraction in (0, 1]; and the three standard
sla_tier values retain their defined ordering. An unknown tier is preserved
but is unranked. Invalid advisory sub-fields are ignored with a diagnostic;
they do not invalidate unrelated manifest fields.free forbids billing_unit, price_hint, and
currency; metered requires a non-empty billing_unit; a price_hint
MUST match ^[A-Z]{3} [0-9]+(\\.[0-9]+)?$, and a separate currency, when
present, MUST equal its prefix. Unknown metering profiles normalize to
metered without inventing a price.{
"nwp": "0.4",
"node_id": "urn:nps:node:api.example.com:orders",
"node_type": "complex",
"display_name": "Order Management Node",
"manifest_version": 7,
"manifest_updated_at": "2026-06-03T00:00:00Z",
"min_agent_version": "0.3",
"wire_formats": ["ncp-capsule", "msgpack", "json"],
"preferred_format": "ncp-capsule",
"schema_anchors": {
"order": "sha256:a3f9b2c1...",
"product": "sha256:b2c1d3e4..."
},
"capabilities": {
"query": true,
"stream_query": true,
"aggregate": true,
"subscribe": true,
"subscribe_filter": true,
"vector_search": false,
"token_budget_hint": true,
"ext_frame": false,
"e2e_enc": false,
"inline_anchor": true
},
"data_sources": ["rds:orders_db"],
"auth": {
"required": true,
"identity_type": "nip-cert",
"trusted_issuers": ["https://ca.mycompany.com"],
"required_capabilities": ["nwp:query", "nwp:invoke"],
"scope_check": "prefix"
},
"rate_limits": {
"requests_per_minute": 300,
"requests_per_day": 50000,
"max_concurrent_streams": 10,
"max_subscriptions": 5
},
"stability": "stable",
"sla": {
"p95_latency_ms": 250,
"availability": "0.999",
"sla_tier": "standard"
},
"billing": {
"metering_profile": "metered",
"billing_unit": "per-request",
"price_hint": "USD 0.0002",
"currency": "USD"
},
"actions": {
"orders.create": {
"description": "Create a new order",
"params_anchor": "sha256:create_params...",
"result_anchor": "sha256:order...",
"async": true,
"idempotent": true,
"timeout_ms_default": 10000,
"timeout_ms_max": 60000,
"required_capability": "nwp:invoke",
"stability": "stable",
"sla": { "p95_latency_ms": 800, "sla_tier": "premium" },
"billing": { "metering_profile": "metered", "billing_unit": "per-request", "price_hint": "USD 0.001" }
},
"orders.cancel": {
"description": "Cancel an existing order",
"params_anchor": "sha256:cancel_params...",
"result_anchor": "sha256:cancel_result...",
"async": false,
"idempotent": true,
"timeout_ms_default": 5000,
"timeout_ms_max": 10000,
"required_capability": "nwp:invoke",
"stability": "experimental"
}
},
"endpoints": {
"query": "nwp://api.example.com/orders/query",
"stream": "nwp://api.example.com/orders/stream",
"invoke": "nwp://api.example.com/orders/invoke",
"subscribe": "nwp://api.example.com/orders/subscribe",
"actions": "nwp://api.example.com/orders/actions",
"schema": "nwp://api.example.com/orders/.schema"
},
"tokenizer_support": ["cl100k_base", "claude"]
}
NWM Conditional Requests
Agents SHOULD cache the NWM and use manifest_version for conditional requests: in HTTP mode via the If-None-Match: <manifest_version> header (integer string, e.g. If-None-Match: 7); if the manifest is unchanged the server returns 304 Not Modified. Servers MUST include X-NWM-Version: <manifest_version> on every GET /.nwm response so agents can detect staleness without a full re-fetch. manifest_updated_at provides a human-readable timestamp of the last structural change.
The actions field is an {action_id: ActionSpec} dictionary. Action/Complex/Anchor Nodes MUST declare all callable operations here.
ActionSpec Field Definitions
| Field | Type | Required | Description |
|---|---|---|---|
description |
string | Optional | Human-readable description of the operation |
params_anchor |
string | Optional | anchor_id of the parameter Schema (Agent uses this to validate ActionFrame.params) |
result_anchor |
string | Optional | anchor_id of the result Schema (CapsFrame uses this anchor_ref on success) |
async |
bool | Required | Whether async execution is supported (if true, ActionFrame may set async=true) |
idempotent |
bool | Optional | Whether the operation is idempotent (Agents may safely retry if true) |
timeout_ms_default |
uint32 | Optional | Default timeout in milliseconds |
timeout_ms_max |
uint32 | Optional | Maximum allowed timeout in milliseconds |
required_capability |
string | Optional | NIP capability required to invoke this operation, e.g. "nwp:invoke" |
min_assurance_level |
string | Optional | Per-action assurance-level override: "anonymous" / "attested" / "verified". When present, takes precedence over the top-level NWM min_assurance_level for requests targeting this action. Requests presenting a lower level MUST be rejected with NWP-AUTH-ASSURANCE-TOO-LOW. (NPS-RFC-0003) |
stability |
string | Optional | Per-action lifecycle stage override ("experimental" / "stable" / "deprecated"). When present, takes precedence over the top-level NWM stability for this action. Marketplace clients SHOULD surface deprecated actions even when the node-level stability is "stable". |
sla |
object | Optional | Per-action SLO override; same shape as the top-level sla field (§4.4a). When present, fields supplied here override the matching top-level fields for this action only; unsupplied sub-fields fall through to the top-level. |
billing |
object | Optional | Per-action commercial-metadata override; same shape as the top-level billing field (§4.4b). Field-level fallback semantics match sla. |
/actions Endpoint
An Agent sends GET /actions; the node returns the full NWM actions field as JSON (for dynamic discovery without downloading the entire NWM):
{
"node_id": "urn:nps:node:api.example.com:orders",
"actions": {
"orders.create": { ... },
"orders.cancel": { ... }
}
}
Agents retrieve a Node’s schema via the following flow (AnchorFrames are published by Nodes; Agents are read-only consumers):
Agent Node
│ │
│── GET /.nwm ─────────────────→ │ 1. Read manifest, get schema_anchors
│←── NWM JSON ────────────────── │ { "order": "sha256:a3f9..." }
│ │
│── GET /.schema ──────────────→ │ 2. Fetch complete AnchorFrame (on demand)
│←── AnchorFrame JSON ────────── │ Agent caches locally
│ │
│── QueryFrame(anchor_ref) ────→ │ 3. Query carries only anchor_ref
│←── CapsFrame(anchor_ref) ───── │
Agents SHOULD preload AnchorFrames for all schema_anchors declared in the NWM on first connection, reducing latency for subsequent requests.
Used for structured data queries on Memory Nodes.
| Field | Type | Required | Description |
|---|---|---|---|
frame |
uint8 | Required | Fixed value 0x10 |
type |
string | Optional | Reserved query type identifier per §12. When set, type-specific fields apply and anchor_ref semantics are defined by the type. Absent: per-anchor query behavior below |
anchor_ref |
string | Conditionally Required | Schema anchor_id; may be omitted for aggregation queries or when type selects a reserved type that does not require it |
auto_anchor |
bool | Optional | If true and the anchor is stale, the Node automatically attaches the latest AnchorFrame in the response. Default: true |
stream |
bool | Optional | If true, triggers streaming query mode (see §6.6); response is a StreamFrame sequence rather than a CapsFrame |
aggregate |
object | Optional | Aggregation operation (see §6.7); when set, anchor_ref may be omitted |
filter |
object | Optional | Filter conditions, see §6.2 |
fields |
array | Optional | List of fields to return; omit to return all fields |
limit |
uint32 | Optional | Maximum records to return, default 20, max 1000; for streaming queries, max records per frame |
cursor |
string | Optional | Pagination cursor from the previous response’s next_cursor |
order |
array | Optional | Sort rules, see §6.3 |
vector_search |
object | Optional | Vector similarity search, see §6.4 |
token_budget |
uint32 | Optional | CGN budget limit (native mode equivalent of X-NWP-Budget) |
tokenizer |
string | Optional | Tokenizer identifier in use (native mode equivalent of X-NWP-Tokenizer) |
depth |
uint8 | Optional | Node graph traversal depth, default 1, max 5 (native mode equivalent of X-NWP-Depth) |
request_id |
string | Optional | UUID v4 for request tracing; echoed back by the node in response and logs |
| Operator | Meaning | Example |
|---|---|---|
$eq |
Equal | { "status": { "$eq": "active" } } |
$ne |
Not equal | { "status": { "$ne": "deleted" } } |
$lt |
Less than | { "price": { "$lt": 500 } } |
$lte |
Less than or equal | { "price": { "$lte": 500 } } |
$gt |
Greater than | { "stock": { "$gt": 0 } } |
$gte |
Greater than or equal | { "rating": { "$gte": 4.0 } } |
$in |
In list | { "category": { "$in": ["phone", "tablet"] } } |
$nin |
Not in list | { "tag": { "$nin": ["discontinued"] } } |
$contains |
String contains (case-sensitive) | { "name": { "$contains": "Pro" } } |
$between |
Range (inclusive on both ends) | { "price": { "$between": [100, 500] } } |
$exists |
Field exists check | { "thumbnail": { "$exists": true } } |
$regex |
Regex match (UTF-8) | { "sku": { "$regex": "^PROD-[0-9]{4}$" } } |
$and |
Logical AND | { "$and": [ {...}, {...} ] } |
$or |
Logical OR | { "$or": [ {...}, {...} ] } |
$not |
Logical NOT | { "$not": { "status": { "$eq": "deleted" } } } |
$regex Security Constraints: Pattern length ≤ 256 characters; nested quantifiers (e.g. (a+)+) are prohibited; nodes MUST perform ReDoS detection and return NWP-QUERY-REGEX-UNSAFE on violation.
Filter nesting depth MUST be ≤ 8 levels.
{ "order": [{ "field": "price", "dir": "ASC" }, { "field": "name", "dir": "ASC" }] }
{
"vector_search": {
"field": "embedding",
"vector": [0.12, -0.34, 0.56],
"top_k": 10,
"threshold": 0.85,
"metric": "cosine"
}
}
Supported metric values: cosine (default), euclidean, dot_product. Nodes declare support via capabilities.vector_search=true; unsupported nodes return NWP-QUERY-VECTOR-UNSUPPORTED.
{
"frame": "0x10",
"anchor_ref": "sha256:a3f9b2c1...",
"auto_anchor": true,
"filter": {
"$and": [
{ "category": { "$eq": "electronics" } },
{ "price": { "$lt": 500 } },
{ "stock": { "$gt": 0 } }
]
},
"fields": ["id", "name", "price", "stock"],
"limit": 20,
"order": [{ "field": "price", "dir": "ASC" }],
"token_budget": 800,
"tokenizer": "cl100k_base",
"request_id": "550e8400-e29b-41d4-a716-446655440001"
}
When QueryFrame contains stream: true (or uses the /stream sub-path), the node responds with a StreamFrame (0x03) sequence rather than a single CapsFrame. Requires capabilities.stream_query=true.
Streaming Query Flow
Agent Node
│ │
│── QueryFrame(stream:true) ────→ │
│ │ Query in batches, limit records per batch
│ ←── StreamFrame(seq=0) ─────── │ First frame, contains anchor_ref and estimated_total
│ ←── StreamFrame(seq=1) ─────── │ Subsequent frames, data is the next batch of records
│ ... │
│ ←── StreamFrame(is_last=true) ─ │ Final frame, is_last=true, data may be empty
First Frame Additional Fields (StreamFrame Extension)
For streaming queries, the first frame (seq=0) SHOULD include metadata:
| Field | Type | Description |
|---|---|---|
estimated_total |
uint64 | Estimated total records matching the filter; -1 means unknown |
request_id |
string | Echo of the QueryFrame’s request_id |
Pagination vs. Streaming
cursor; records are pushed continuously per order until limit × frames or full push completesrequest_id, or disconnects. Nodes route the cancellation by request_id and MUST NOT require a SubscribeFrame-shaped cancel message for streaming queries.When QueryFrame contains an aggregate field, the node returns aggregated results rather than raw records. Requires capabilities.aggregate=true.
aggregate Field Definitions
| Field | Type | Required | Description |
|---|---|---|---|
operations |
array | Required | List of aggregation operations, see below |
group_by |
array | Optional | Grouping field list, e.g. ["category", "status"] |
having |
object | Optional | Post-grouping filter (same syntax as filter, but field names are aliases) |
operation Element
| Field | Type | Required | Description |
|---|---|---|---|
func |
string | Required | COUNT / SUM / AVG / MIN / MAX / COUNT_DISTINCT |
field |
string | Conditionally Required | Field to aggregate (COUNT may omit, meaning row count) |
alias |
string | Required | Result field name |
Aggregation Query Example
{
"frame": "0x10",
"filter": { "status": { "$eq": "active" } },
"aggregate": {
"operations": [
{ "func": "COUNT", "alias": "total" },
{ "func": "SUM", "field": "price", "alias": "revenue" },
{ "func": "AVG", "field": "rating", "alias": "avg_rating" }
],
"group_by": ["category"],
"having": { "total": { "$gt": 10 } }
},
"order": [{ "field": "revenue", "dir": "DESC" }],
"request_id": "550e8400-e29b-41d4-a716-446655440002"
}
Aggregation Response (CapsFrame)
Aggregation responses do not use a business schema; anchor_ref is fixed as "nps:system:aggregate:result":
{
"frame": "0x04",
"anchor_ref": "nps:system:aggregate:result",
"count": 3,
"data": [
{ "category": "electronics", "total": 142, "revenue": 89450.00, "avg_rating": 4.3 },
{ "category": "clothing", "total": 87, "revenue": 12300.00, "avg_rating": 4.1 },
{ "category": "books", "total": 56, "revenue": 3200.00, "avg_rating": 4.6 }
]
}
Used for operation invocation on Action Nodes and Complex Nodes.
| Field | Type | Required | Description |
|---|---|---|---|
frame |
uint8 | Required | Fixed value 0x11 |
action_id |
string | Required | Operation identifier, format: {domain}.{verb}; system-reserved operations see §7.3 |
params |
object | Optional | Operation parameters; schema defined by NWM actions.{action_id}.params_anchor |
idempotency_key |
string | Optional | Idempotency key (UUID v4), valid for 24 hours |
timeout_ms |
uint32 | Optional | Timeout in milliseconds, default 5000, max 300000 |
async |
bool | Optional | If true, execute asynchronously; response returns task_id |
callback_url |
string | Optional | Callback URL for async task completion (https:// only) |
priority |
string | Optional | Task priority: "low" / "normal" (default) / "high" |
request_id |
string | Optional | UUID v4 for request tracing (echoed in response and task status) |
PENDING → RUNNING → COMPLETED
↘ FAILED
↘ CANCELLED
For async execution, the initial response (CapsFrame):
{
"task_id": "uuid-v4",
"status": "pending",
"poll_url": "nwp://api.example.com/orders/actions/status/uuid-v4",
"estimated_ms": 3000,
"request_id": "550e8400-..."
}
All nodes supporting async Actions MUST implement:
| action_id | Description | Required Params | Response |
|---|---|---|---|
system.task.status |
Poll task status | { "task_id": "uuid" } |
Task status object (see below) |
system.task.cancel |
Cancel a task | { "task_id": "uuid" } |
{ "cancelled": true } or error |
system.task.status Response
{
"task_id": "uuid-v4",
"status": "running",
"progress": 0.42,
"created_at": "2026-04-14T10:00:00Z",
"updated_at": "2026-04-14T10:00:05Z",
"request_id": "550e8400-...",
"result": null,
"error": null
}
{
"frame": "0x11",
"action_id": "orders.create",
"params": { "product_id": 1001, "quantity": 2 },
"idempotency_key": "550e8400-e29b-41d4-a716-446655440000",
"timeout_ms": 10000,
"async": true,
"callback_url": "https://agent.myapp.com/callbacks/nwp",
"priority": "normal",
"request_id": "550e8400-e29b-41d4-a716-446655440003"
}
llm.complete)NWP standardizes the llm.complete action so SDKs and agent runtimes do not
need private ActionFrame payload codecs for ordinary model completions.
Nodes that serve this action SHOULD also advertise the LLM/Thinking Profile in
NWM profiles.llm (§4.2a) so clients can discover models, streaming/tool
support, privacy hints, and reasoning-disclosure policy without invoking a
model request.
Request binding
ActionFrame with action_id = "llm.complete".ActionFrame.params MUST contain a LlmCompleteActionRequest object.params.kind MUST be "llm.complete" when present. Producers SHOULD emit it
for self-description and compatibility with pre-contract clients; consumers
MAY accept an absent kind only when action_id is already "llm.complete".ActionFrame.async = true requests asynchronous execution. params.stream =
true requests an immediate StreamFrame response. The two flags MUST NOT be
combined; servers SHOULD reject the combination with NWP-ACTION-PARAMS-INVALID.LlmCompleteActionRequest fields
| Field | Type | Required | Description |
|---|---|---|---|
kind |
string | Optional | Self-description. Canonical value: "llm.complete" |
model |
string | Required | Model identifier as understood by the receiving LLM node |
max_tokens |
uint32 | Optional | Maximum generated tokens |
stream |
bool | Optional | When true, response is a StreamFrame sequence instead of a synchronous CapsFrame |
messages |
array | Required | Ordered conversation messages |
tools |
array | Optional | Tool definitions available to the model |
context |
LlmContextRequestDto |
Optional | Stateful context operation defined in §7.6. Absent preserves stateless full-history semantics. |
LlmMessageDto fields
| Field | Type | Required | Description |
|---|---|---|---|
role |
string | Required | "system" / "user" / "assistant" / "tool" |
content |
string | Optional | Message text or serialized tool result |
tool_call_id |
string | Optional | Tool-call id referenced by a "tool" message |
tool_name |
string | Optional | Tool name referenced by a "tool" message |
tool_calls |
array | Optional | Tool calls emitted by an assistant message |
Tool fields
LlmToolCallDto uses { "call_id", "tool_name", "arguments_json" }.
arguments_json is a JSON string so providers can preserve the exact argument
object received from or sent to an LLM backend. LlmToolDefinitionDto uses
{ "name", "description", "parameters" }, where each ToolParameterDto uses
{ "name", "type", "description", "required" }. Standard type values are
"string", "number", "boolean", "object", and "array".
Success response semantics
| Request mode | Successful response |
|---|---|
async=false, stream=false |
CapsFrame with anchor_ref = "nps:system:llm.complete:response", request_id copied from the ActionFrame when present, and data[0] containing LlmCompleteActionResponse |
async=true, stream=false |
AsyncActionResponse acknowledgment. system.task.status.result contains LlmCompleteActionResponse when completed |
stream=true |
StreamFrame sequence with anchor_ref = "nps:system:llm.complete:stream" on the first chunk; data[] contains LlmCompleteStreamChunkDto items |
Fire-and-forget is not a separate llm.complete mode. Clients that do not care
about the result MAY submit an async request and ignore the poll URL, but the
server still follows the async task contract.
LlmCompleteActionResponse fields
| Field | Type | Required | Description |
|---|---|---|---|
stop_reason |
enum | Required | "end_turn" / "tool_use" / "tool_calls" / "max_tokens" / "length" / "error" |
content |
string | Optional | Final generated text for non-tool completions |
tool_calls |
array | Optional | Tool calls requested by the model |
error |
string | Optional | Model/provider-level completion error; see error rule below |
usage |
object | Optional | Actual model/provider usage for this invocation; see LlmUsageDto below |
context |
LlmContextReceiptDto |
Optional | Committed stateful-context receipt; MUST be absent for stateless requests. |
For streaming responses, LlmCompleteStreamChunkDto.content_delta carries the
new text for that chunk. The final chunk SHOULD set stop_reason; abnormal
stream termination SHOULD use a terminal ErrorFrame or StreamFrame.error_code.
The terminal chunk MAY carry usage. A successful stateful stream MUST carry
its context receipt only on the terminal chunk. Non-terminal chunks MUST omit
the receipt and SHOULD omit usage.
LlmUsageDto fields
| Field | Type | Required | Description |
|---|---|---|---|
input_tokens |
uint32 | Optional | Total logical model-input tokens, including any reused prefix |
output_tokens |
uint32 | Optional | Tokens generated by the model for this completion |
cache_hit |
bool | Optional | Whether the model runtime reused a prefix/KV-cache entry |
reused_tokens |
uint32 | Optional | Input tokens reused from prefix/KV cache without new evaluation |
evaluated_tokens |
uint32 | Optional | Input tokens newly evaluated by the model for this invocation |
wire_input_bytes |
uint64 | Optional | Complete serialized ActionFrame payload bytes measured at the NWP decoder boundary, after NCP decryption and excluding the NCP header/TLS/response bytes |
Every usage field is optional because providers expose different accounting
levels. Values in usage MUST be actual runtime/provider observations, not
estimates. CapsFrame.token_est remains a CGN estimate and is not a substitute
for usage. CapsFrame.cached means the complete NWP response came from the
server-side response cache; it is distinct from usage.cache_hit. When all
three input-accounting fields are known, producers SHOULD satisfy
reused_tokens + evaluated_tokens = input_tokens.
cache_hit = true is valid only when reused_tokens > 0. wire_input_bytes
MUST be measured from the accepted wire payload, not obtained by re-serializing
a DTO. Providers MUST omit token fields they cannot observe rather than derive
them from prompt length.
Error rule
Protocol, validation, authorization, timeout, and provider dispatch failures
SHOULD be returned as ErrorFrame using the normal NWP error mapping. The
LlmCompleteActionResponse.error field is reserved for model-level failures
that are themselves a successful action result, for example a provider returning
a structured “model refused/error” completion instead of raising a transport or
server error.
Field naming and encoding
Canonical JSON field names are snake_case. SDK producers MUST emit snake_case. SDK consumers SHOULD accept PascalCase/case-insensitive property names as a compatibility fallback. MessagePack payload maps MUST use the same canonical snake_case keys as JSON.
This section implements NPS-CR-0011. It is an opt-in extension of §7.5, not a
new frame family. A request without LlmCompleteActionRequest.context remains
stateless and sends a complete ordered message list. A request with context
MUST execute the requested state transition exactly or fail with an ErrorFrame;
the server MUST NOT silently rebuild a stateless prompt or report a false cache
hit.
A stateful llm.complete ActionFrame MUST carry an idempotency_key. Its
messages field means the complete initial/replacement transcript for create
and reset, and only the ordered delta after base_version for append and
fork. An empty message array is allowed only for fork.
LlmContextRequestDto
| Field | Type | Required | Description |
|---|---|---|---|
operation |
enum | Required | create, append, fork, or reset. |
context_id |
string | Conditional | Required for all operations except create; forbidden for create. |
base_version |
uint64 | Conditional | Required for append, fork, and reset; MUST equal the committed version. |
ttl_seconds |
uint32 | Optional | Requested idle TTL; the server MAY clamp it to its advertised maximum. |
ttl_seconds MUST be greater than zero when present. If omitted, create uses
the node default; append and reset preserve the context’s current effective
TTL; and fork inherits the source context’s remaining TTL. A supplied value or
an inherited/default value MAY be clamped to max_ttl_seconds; the effective
expiry MUST be returned in the receipt when TTL is bounded.
Context IDs are case-sensitive, unpadded base64url strings generated from at
least 128 bits of cryptographically secure randomness. Producers MUST emit
22–128 ASCII characters from [A-Za-z0-9_-]. An ID MUST NOT encode a NID,
tenant, model, database key, or other sensitive metadata. It is a locator, not
an authorization credential.
LlmContextReceiptDto
| Field | Type | Required | Description |
|---|---|---|---|
context_id |
string | Required | Opaque context identifier. |
version |
uint64 | Required | Committed version after the operation. Create/fork start at 1; append/reset/release increment exactly once. |
operation |
enum | Required | create, append, fork, reset, or release. |
state |
enum | Required | active for completion mutations; released for release. |
expires_at |
RFC 3339 timestamp | Optional | Effective idle expiry when bounded. |
parent_context_id |
string | Required for fork | Source context. |
parent_version |
uint64 | Required for fork | Immutable source version used by the fork. |
Unary success carries the receipt on LlmCompleteActionResponse.context.
Async acknowledgment does not commit context state; the receipt appears only in
the completed system.task.status.result. Streaming success carries the receipt
only on the terminal chunk. Stateless responses and non-terminal chunks MUST
omit it.
| Action | Request DTO | Success response |
|---|---|---|
llm.context.status |
LlmContextStatusRequestDto with exactly one of context_id or idempotency_key |
CapsFrame anchor_ref = "nps:system:llm.context.status:response", data[0] = LlmContextStatusDto |
llm.context.release |
LlmContextReleaseRequestDto with context_id and base_version; ActionFrame MUST carry idempotency_key |
CapsFrame anchor_ref = "nps:system:llm.context.release:response", data[0] = LlmContextReceiptDto |
LlmContextStatusDto contains required state (busy, active, released,
expired, or failed) plus optional context_id, version, expires_at,
request_id, and error_code. Active/released/expired states MUST carry an ID
and version. An in-flight create reports busy but MUST omit its not-yet-
committed ID/version. A failed create reports failed, omits ID/version, and
carries its terminal error code. Status is observational and MUST NOT refresh
TTL.
Release increments vN to a released tombstone vN+1. It is replay-idempotent for
the same owner and ActionFrame key. Mutations against a released context return
NWP-LLM-CONTEXT-NOT-FOUND; the owner MAY still inspect its tombstone with
status during the advertised retention window.
Create/reset binds the context to the resolved model ID, ordered system
messages, canonical tool definitions, and the provider/runtime compatibility
revision required for prefix reuse. Append/fork MUST preserve this binding.
Their deltas MUST NOT contain system-role messages; omitted tools reuses the
bound definitions, while present tools MUST canonically equal them. The
required model field MUST resolve to the bound model. A mismatch returns
NWP-LLM-CONTEXT-BINDING-MISMATCH.
Stateful llm.complete mutations require both llm:complete and
llm:context; streaming and tools additionally require llm:stream and
llm:tool_call, respectively. Status/release require llm:context plus owner
authorization but do not require the caller to retain model-invocation rights.
Stateful coordinator APIs MUST pass the complete required-capability set to the
deployment-owned NIP authorizer at every admission and commit check. A
coordinator with no authorizer installed MUST fail closed with
NWP-LLM-CONTEXT-FORBIDDEN; it MUST NOT treat an absent hook as authorization.
The owner is the authenticated NID plus the node’s authenticated tenant/workspace
security scope. That scope comes from admitted identity and deployment policy,
never a client-controlled context field. Every operation MUST re-run normal NIP
expiry, revocation, assurance, scope, and capability checks. Long-running work
MUST repeat revocation/authorization checks before commit. An authenticated
non-owner receives NWP-LLM-CONTEXT-FORBIDDEN.
Create commits v1. Append/reset compare-and-swap base_version and commit
vN+1. Fork atomically snapshots the parent at admission, creates a child v1,
and never mutates the parent. A later parent mutation does not invalidate an
admitted fork snapshot. At most one mutation reservation may exist per context;
a stale or concurrent loser receives NWP-LLM-CONTEXT-VERSION-CONFLICT with
the current version in the ErrorFrame hint.
A completion mutation commits only after a successful terminal result whose
stop reason is not error. The commit includes both the request delta and the
terminal assistant text/tool calls. A structured refusal represented by an
ordinary end_turn MAY commit. Validation/auth/provider failure, timeout,
cancellation, stop_reason = "error", or abnormal stream termination MUST
leave the prior transcript/version unchanged and release the reservation.
The server MUST atomically commit to the store promised by its NWM persistence level before exposing the terminal receipt. A valid reservation prevents idle expiry while work is running. Successful create/append/fork/reset starts or refreshes TTL; status and failed/cancelled mutations do not. If the prior TTL elapsed during work and the reservation aborts, the context transitions directly to expired. Automatic expiry records the last committed version without incrementing it.
The NWM persistence values mean:
connection: survives requests only on the creating NCP connection;process: survives reconnect only when routed to the same process, not a
process restart or another instance;durable: survives process restart under the same logical node NID and
endpoint identity.Connection/process contexts are node-instance scoped and clients SHOULD pin them to the creating endpoint. Cross-instance migration/distributed context stores are not defined here. State loss returns not-found/expired and MUST NOT create a replacement.
The existing 24-hour ActionFrame idempotency window also retains the owner-
scoped key-to-outcome record, even when context TTL is shorter. A duplicate
while the first request runs returns NWP-ACTION-IDEMPOTENCY-CONFLICT and MUST
NOT join a live stream. Completed unary/async requests use the cached-result
rule. Completed streaming replay uses a new StreamFrame sequence with logically
identical ordered text, tool calls, stop reason, usage, and terminal receipt;
it MUST NOT regenerate or recommit. llm.context.status by idempotency key
resolves a lost create response. After 24 hours the key lookup MAY return
not-found.
Released/expired tombstones SHOULD remain visible to their owner for at least
tombstone_seconds. Mutations against an expiry tombstone return
NWP-LLM-CONTEXT-EXPIRED; after tombstone removal they return not-found.
| Error | NPS status | Meaning |
|---|---|---|
NWP-LLM-CONTEXT-NOT-FOUND |
NPS-CLIENT-NOT-FOUND |
Unknown/released context or expired idempotency lookup. |
NWP-LLM-CONTEXT-EXPIRED |
NPS-CLIENT-GONE |
Idle-expiry tombstone is still present. |
NWP-LLM-CONTEXT-VERSION-CONFLICT |
NPS-CLIENT-CONFLICT |
Stale version or concurrent mutation reservation. |
NWP-LLM-CONTEXT-BINDING-MISMATCH |
NPS-CLIENT-CONFLICT |
Model/system/tools/runtime binding differs. |
NWP-LLM-CONTEXT-FORBIDDEN |
NPS-AUTH-FORBIDDEN |
Caller is not owner or lacks scope/capability. |
NWP-LLM-CONTEXT-LIMIT-EXCEEDED |
NPS-LIMIT-RESOURCE |
Per-principal live-context limit reached. |
NWP-LLM-CONTEXT-OPERATION-UNSUPPORTED |
NPS-SERVER-UNSUPPORTED |
Node supports context but not this operation. |
Malformed field combinations use NWP-ACTION-PARAMS-INVALID. All failures in
this section use ErrorFrame or terminal stream error, never
LlmCompleteActionResponse.error.
All six SDKs MUST execute conformance/nwp/llm_context_vectors.json. A benchmark
claiming stateful savings MUST compare the same multi-turn role/tool semantics
in stateless and strict-native stateful modes. On the second turn, stateful mode
MUST send only the delta and demonstrate both lower wire_input_bytes and lower
observed evaluated_tokens. Protocol fallback MUST be disabled.
Used to establish change subscriptions on Memory and Anchor Nodes. The authoritative v0.13 wire shape is defined in §13 (CR-0006): subscription_id, filter, heartbeat_interval_ms, max_events, and opaque cursor.
Earlier alpha drafts used action, stream_id, heartbeat_interval, and resume_from_seq. Those names are retired for NWP v0.13 and MUST NOT be emitted by conformant alpha.11+ producers. Consumers MAY accept them only as a pre-alpha.11 compatibility fallback, but MUST normalize internally to the §13 fields.
POST /nwp/orders/subscribe HTTP/1.1
Content-Type: application/nwp-frame
[SubscribeFrame bytes]
HTTP/1.1 200 OK
Content-Type: text/event-stream
data: [DiffFrame bytes, base64]
data: [DiffFrame bytes, base64]
| Header | Required | Description |
|---|---|---|
X-NWP-Agent |
Conditionally Required | Agent NID (required when auth.required=true) |
Authorization |
Conditionally Required | HTTP bearer credential when auth.identity_type = "bearer". Not used in native mode; native authentication is session-bound via NCP/NIP IdentFrame. |
X-NWP-Budget |
Optional | CGN budget limit (uint32) |
X-NWP-Tokenizer |
Optional | Tokenizer identifier used by the Agent |
X-NWP-Depth |
Optional | Node graph traversal depth, default 1, max 5 |
X-NWP-Encoding |
Optional | Request encoding tier: json/msgpack, default msgpack |
X-NWP-Request-ID |
Optional | UUID v4 request tracing ID; echoed back by the node in the response header |
If-None-Match |
Optional | NWM conditional request; value is manifest_version |
Content-Type |
Required | application/nwp-frame |
| Header | Description |
|---|---|
X-NWP-Schema |
anchor_id used in the response |
X-NWP-Tokens |
Actual CGN consumed |
X-NWP-Tokens-Native |
Native token consumption |
X-NWP-Tokenizer-Used |
Tokenizer actually used |
X-NWP-Cached |
true indicates a cache hit |
X-NWP-Node-Type |
Node type |
X-NWP-Request-ID |
Echo of the requester’s X-NWP-Request-ID (node MAY auto-generate if not provided) |
X-NWP-Rate-Limit |
Max requests per minute |
X-NWP-Rate-Remaining |
Remaining requests this minute |
X-NWP-Rate-Reset |
Rate limit window reset time (Unix timestamp) |
Content-Type |
application/nwp-capsule (normal response) / application/nwp-error+json (error response) |
| HTTP Header | QueryFrame Field | ActionFrame Field |
|---|---|---|
X-NWP-Agent |
— (declared as HelloFrame agent_id hint; authenticated identity is the session-bound NIP IdentFrame) |
Same |
X-NWP-Budget |
token_budget |
— |
X-NWP-Tokenizer |
tokenizer |
— |
X-NWP-Depth |
depth |
— |
X-NWP-Request-ID |
request_id |
request_id |
In HTTP mode, error responses use the following format, Content-Type: application/nwp-error+json:
{
"status": "NPS-CLIENT-NOT-FOUND",
"error": "NWP-ACTION-NOT-FOUND",
"message": "Action 'orders.ship' is not registered on this node",
"details": { "action_id": "orders.ship" },
"request_id": "550e8400-e29b-41d4-a716-446655440003"
}
The HTTP status code is determined by the NPS status code mapping; see status-codes.md.
Field Descriptions
| Field | Type | Required | Description |
|---|---|---|---|
status |
string | Required | NPS status code |
error |
string | Required | Protocol-level error code (e.g. NWP-ACTION-NOT-FOUND) |
message |
string | Optional | Human-readable description |
details |
object | Optional | Structured additional error information |
request_id |
string | Optional | Echo of the X-NWP-Request-ID from the request |
HTTP overlay implementations MUST use canonical NWP error codes for transport-binding preconditions before the request body is admitted as a NWP frame. These errors are NWP-owned because they determine whether an NWP QueryFrame, ActionFrame, or SubscribeFrame can be recovered from the HTTP exchange; they are not implementation-local adapter errors.
| Rejection | Trigger | Error Code | NPS Status Code |
|---|---|---|---|
| Origin disallowed | Browser-facing HTTP binding rejects the request origin by CORS or equivalent origin policy | NWP-HTTP-ORIGIN-FORBIDDEN |
NPS-AUTH-FORBIDDEN |
| Content-Type unsupported | Request body media type is not a supported NWP frame media type | NWP-HTTP-CONTENT-TYPE-UNSUPPORTED |
NPS-CLIENT-BAD-FRAME |
| Accept unsatisfiable | Accept refuses every response media type this node can emit |
NWP-HTTP-ACCEPT-UNSATISFIABLE |
NPS-CLIENT-BAD-PARAM |
| Request-id echo mismatch | Client observes that the response X-NWP-Request-ID does not match the request header |
NWP-HTTP-REQUEST-ID-MISMATCH |
NPS-CLIENT-BAD-PARAM |
| Unparseable frame body | HTTP body cannot be parsed as any supported NWP frame envelope or NCP-carried NWP frame | NWP-HTTP-FRAME-BODY-MALFORMED |
NPS-CLIENT-BAD-FRAME |
| Body too large | Request body exceeds the server’s advertised or configured NWP body limit | NWP-HTTP-BODY-TOO-LARGE |
NPS-LIMIT-PAYLOAD |
| Advertised but unimplemented | NWM advertises a capability or profile that this node accepts in discovery but cannot currently serve | NWP-CAPABILITY-ADVERTISED-UNIMPLEMENTED |
NPS-SERVER-UNSUPPORTED |
NWP-CAPABILITY-ADVERTISED-UNIMPLEMENTED is distinct from capability-specific unsupported codes such as NWP-QUERY-VECTOR-UNSUPPORTED: use the specific code when the manifest truthfully declares the feature unsupported, and use NWP-CAPABILITY-ADVERTISED-UNIMPLEMENTED only for rollout windows, disabled backends, or inconsistent discovery state where the feature was advertised but cannot be served.
HTTP Mode Query Request
POST /nwp/orders/query HTTP/1.1
Host: api.example.com:17433
X-NWP-Agent: urn:nps:agent:ca.innolotus.com:550e8400
X-NWP-Budget: 1200
X-NWP-Tokenizer: cl100k_base
X-NWP-Request-ID: 550e8400-e29b-41d4-a716-446655440001
Content-Type: application/nwp-frame
[QueryFrame: { anchor_ref, filter, fields, limit, auto_anchor }]
Success Response
HTTP/1.1 200 OK
X-NWP-Schema: sha256:a3f9...
X-NWP-Tokens: 380
X-NWP-Request-ID: 550e8400-e29b-41d4-a716-446655440001
X-NWP-Rate-Limit: 300
X-NWP-Rate-Remaining: 248
Content-Type: application/nwp-capsule
[CapsFrame]
Error Response
HTTP/1.1 404 Not Found
X-NWP-Request-ID: 550e8400-e29b-41d4-a716-446655440001
Content-Type: application/nwp-error+json
{ "status": "NPS-CLIENT-NOT-FOUND", "error": "NWP-QUERY-FIELD-UNKNOWN", ... }
Complex Nodes declare sub-node references in the NWM:
{
"graph": {
"refs": [
{ "rel": "user", "node": "nwp://api.myapp.com/users" },
{ "rel": "payment", "node": "nwp://pay.myapp.com/transactions" }
],
"max_depth": 2
}
}
Agents control traversal depth via the X-NWP-Depth header (HTTP mode) or the QueryFrame depth field (native mode). Nodes MUST detect circular references (return NWP-GRAPH-CYCLE) and maintain a sub-node URL whitelist (SSRF prevention).
The type field on QueryFrame (§6.1) and SubscribeFrame (§13.1) opts a request into a reserved query type with specification-defined semantics. Identifiers in the topology.* namespace are reserved for cluster topology operations on Anchor Nodes; all reserved namespaces below are mandatory for the node roles indicated.
| Namespace | Owning role | Mandatory at | Operations |
|---|---|---|---|
topology.* |
Anchor Node (§2.1) | NPS-AaaS Profile L2 (services/NPS-AaaS-Profile.md §4.3) | topology.snapshot (§12.1), topology.stream (§12.2) |
When type is absent, default per-anchor query/subscribe semantics apply (§6, §8). When type is set, the per-type fields documented below apply and any conflicting standard fields (e.g. anchor_ref, top-level filter) are ignored unless the per-type schema explicitly carries them.
Implementations that do not recognize a reserved type value MUST reject the request with NWP-RESERVED-TYPE-UNSUPPORTED (§13) so the caller can distinguish “unknown reserved operation” from “action_id not found” (NWP-ACTION-NOT-FOUND) and fall back or fail explicitly.
topology.snapshotSingle-shot retrieval of an Anchor Node’s cluster topology.
| Property | Value |
|---|---|
| Frame | QueryFrame (0x10) with type = "topology.snapshot" |
| Required of | All Anchor Nodes (mandatory at NPS-AaaS Profile L2 and above) |
| Idempotent | Yes |
| Caching | Responses MAY be cached client-side; the version field correlates the snapshot with subsequent topology.stream events |
Request fields (top-level on QueryFrame, supplementing §6.1):
| Field | Type | Required | Description |
|---|---|---|---|
type |
string | Required | Constant "topology.snapshot" |
topology |
object | Required | Container for topology-specific parameters per below |
topology.scope |
string | Required | "cluster" for the Anchor’s own cluster; "member" for a single member (requires topology.target_nid) |
topology.include |
array of strings | Optional | Subset of ["members", "capabilities", "tags", "metrics"]. Default ["members"]. The capabilities and metrics schemas are implementation-defined and MAY be empty |
topology.depth |
uint8 | Optional | For sub-Anchor members, controls recursion. 1 (default) lists sub-Anchors as references only; 2+ recurses. Anchor Nodes MAY cap depth and set truncated: true when exceeded. Sub-Anchor recursion at depth ≥ 2 is OPTIONAL at L2; clients SHOULD recurse manually by issuing one snapshot per sub-Anchor |
topology.target_nid |
string | Conditionally Required | Required when topology.scope = "member"; the NID of the member to introspect |
Response: CapsFrame (0x04) with anchor_ref = "nps:system:topology:snapshot" and a single-element data array carrying the snapshot payload below.
{
"frame": "0x04",
"anchor_ref": "nps:system:topology:snapshot",
"count": 1,
"data": [{
"version": 142,
"anchor_nid": "urn:nps:node:labacacia:host-abc123",
"cluster_size": 23,
"members": [
{
"nid": "urn:nps:agent:labacacia:host-abc123-sess-aaa",
"node_roles": ["memory"],
"activation_mode": "ephemeral",
"tags": ["dev", "library"],
"joined_at": "2026-04-15T10:23:00Z",
"last_seen": "2026-04-26T14:55:00Z"
},
{
"nid": "urn:nps:node:labacacia:host-def456",
"node_roles": ["anchor"],
"activation_mode": "resident",
"child_anchor": true,
"member_count": 7,
"tags": ["sub-cluster", "training"]
}
],
"truncated": false
}]
}
Snapshot payload fields:
| Field | Type | Required | Description |
|---|---|---|---|
version |
uint64 | Required | Monotonically increasing topology version. Resets only on Anchor restart / rebase (§12.3) |
anchor_nid |
string | Required | NID of the responding Anchor Node |
cluster_size |
uint32 | Required | Total direct members, regardless of depth truncation |
members |
array of member objects | Required | Per the member object schema below |
truncated |
bool | Optional | True iff topology.depth cap was hit; otherwise omitted or false |
Member object schema:
| Field | Type | Required | Description |
|---|---|---|---|
nid |
string | Required | Member NID |
node_roles |
array of strings | Required | NDP node_roles — the full role set declared by this member (NPS-4 §3.1) |
activation_mode |
string | Required | NDP activation_mode — one of ephemeral / resident / hybrid (NPS-4) |
child_anchor |
bool | Optional | True if this member is itself an Anchor Node of a sub-cluster; implies member_count |
member_count |
uint32 | Conditionally Required | Required when child_anchor = true; count of the sub-Anchor’s direct members |
tags |
array of strings | Optional | NDP-declared tags |
joined_at |
string | Optional | RFC 3339 timestamp; first observed |
last_seen |
string | Optional | RFC 3339 timestamp; most recent NDP Announce |
capabilities |
object | Optional | Returned only if requested via topology.include; schema implementation-defined |
metrics |
object | Optional | Returned only if requested via topology.include; schema implementation-defined |
topology.streamContinuous topology change feed for an Anchor Node’s cluster.
| Property | Value |
|---|---|
| Frame | SubscribeFrame (0x12) with type = "topology.stream" |
| Required of | All Anchor Nodes (mandatory at NPS-AaaS Profile L2 and above) |
| Cancelable | Yes — by closing the subscription transport or receiving/sending a terminal ErrorFrame |
Request fields (top-level on SubscribeFrame, supplementing §13.1):
| Field | Type | Required | Description |
|---|---|---|---|
type |
string | Required | Constant "topology.stream" |
subscription_id |
string | Required | Client-generated UUID v4 used to correlate the topology stream and pushed DiffFrames |
cursor |
string | Optional | Opaque server-issued resume cursor from a previous subscription. When present, cursor takes precedence over topology.since_version |
topology |
object | Required | Container for topology-specific parameters per below |
topology.scope |
string | Required | "cluster" (default for Anchor’s own cluster); future scopes reserved |
topology.filter |
object | Optional | Reduces event volume. Supported keys: tags_any (array, match-any), tags_all (array, match-all), node_roles (array — filter by role, matches members whose node_roles intersects the given values). Anchor Nodes MUST reject unsupported filter keys with NWP-TOPOLOGY-FILTER-UNSUPPORTED |
topology.since_version |
uint64 | Optional | Topology-specific initial resume hint for clients that have a snapshot version but do not yet have an opaque cursor. Anchor Node MUST replay missed events when feasible; if the version is outside the retention window, MUST emit a resync_required event and the client MUST issue a fresh topology.snapshot |
For type = "topology.stream", cursor is the canonical v0.13 resume mechanism. topology.since_version is accepted only as a topology-specific bootstrap hint when no cursor is available; when both fields are present, cursor takes precedence.
Events are pushed as DiffFrame (0x02) with the §13.2 subscription event envelope. For topology.stream subscriptions, subscription_id identifies the stream, event_type carries one of the topology event types below (extending the default "create" / "update" / "delete" enum), seq is the post-event topology version (§12.3), and payload carries the event-specific data.
event_type |
Trigger | payload shape |
|---|---|---|
member_joined |
NDP Announce from a node naming this Anchor as cluster_anchor |
Full member object (§12.1) |
member_left |
Member explicitly leaves OR exceeds NDP liveness TTL | { "nid": "urn:nps:..." } |
member_updated |
Existing member’s metadata changes (tags, activation_mode, capabilities, etc.) | { "nid": "urn:nps:...", "changes": { "<field>": <new value>, ... } } — field-level diff only; reassembly is the client’s responsibility |
anchor_state |
Anchor cluster status change relevant to subscribers. Payload carries a discriminator field selecting one of the sub-types below |
{ "field": "<sub-type>", "details": { ... } } |
resync_required |
Subscriber MUST tear down its local view and issue a fresh topology.snapshot followed by a new topology.stream subscription. Triggers: (a) topology.since_version is no longer replayable; (b) any anchor_state sub-type that invalidates the prior version counter (e.g. version_rebased); (c) server-side state loss requiring re-subscription |
{ "reason": "<version_too_old | anchor_rebased | server_state_lost>" }. This event MAY omit seq and the subscriber MUST issue a fresh topology.snapshot |
anchor_state sub-types (selected by the payload field discriminator):
field |
Phase | Trigger | details shape |
|---|---|---|---|
version_rebased |
Phase 1–2 | Anchor restarted and reset its monotonic version counter (§12.3). Subscribers MUST treat as equivalent to resync_required |
{ "previous_version": <uint64>, "new_version": <uint64> } |
anchor_failover |
Finalised (NPS-CR-0009) | Active Anchor handed cluster ownership to a peer (multi-Anchor HA, AaaS L3). Emitted at each ownership transfer; the fenced prior leader sends a terminal anchor_failover then closes its streams |
{ "successor_nid": "urn:nps:...", "cluster_epoch": <uint64>, "reason": "planned" \| "active_lost" } |
anchor_quorum_lost |
Finalised (NPS-CR-0009) | Anchor cluster cannot maintain the ownership quorum; cluster is read-only (degraded). Anchors reject topology writes with NWP-ANCHOR-NOT-LEADER and mark health: "degraded" (NDP §3.2) |
{ "quorum_size": <uint32>, "available": <uint32> } |
Cluster ownership fence (NPS-CR-0009). In a multi-Anchor cluster at most one Anchor is the active owner at any instant, identified by a monotonically increasing cluster_epoch (uint64, starts at 1). Every topology.snapshot / topology.stream response and every topology-mutating write carries the current cluster_epoch. A standby (or read-only-degraded) Anchor MUST reject topology writes with NWP-ANCHOR-NOT-LEADER (→ NPS-CLIENT-CONFLICT); an active Anchor MUST reject any inbound frame bearing a higher cluster_epoch with NWP-ANCHOR-EPOCH-FENCED (fencing a superseded leader). Single-Anchor clusters keep cluster_epoch = 1 and never emit anchor_failover / anchor_quorum_lost. See NPS-CR-0009.
Implementations MUST treat unknown anchor_state.field values as forward-compatible and ignore them rather than tearing down the subscription, so future Phase 3 sub-types can be introduced without a wire break.
Standard SubscribeFrame heartbeats (§13.2) operate unchanged. Cancellation is transport-level for v0.13: either side MAY close the subscription stream after emitting a terminal ErrorFrame when an error reason is available.
Guaranteed:
topology.snapshot returned at version: V reflects the cluster state after exactly V topology mutations.topology.stream event with seq: V reflects the cluster state after exactly V mutations.V combined with all subsequent stream events V+1, V+2, … yields a consistent live view.Not guaranteed:
version counter.Restart and rebase: An Anchor Node MAY rebase its version counter on restart. When rebasing, the Anchor MUST emit an anchor_state event with field: "version_rebased" to all active subscribers; subscribers MUST treat this as equivalent to resync_required and issue a fresh topology.snapshot.
capabilities and metrics field schemas are implementation-defined; standardization is deferred to a follow-up CR once enough implementations exist.Authorization model — minimum binding (Phase 1–2): Anchor Nodes MUST enforce the following minimum before serving any topology.* request:
topology.snapshot (single-shot pull, §12.1): the requesting NID MUST declare topology:read in IdentFrame.capabilities (NPS-3 §5.1); absent capability MUST produce NWP-TOPOLOGY-UNAUTHORIZED.topology.stream (long-lived subscription, §12.2): the requester MUST declare topology:read AND topology:subscribe in IdentFrame.capabilities. Phase 2 Anchor Nodes MUST enforce the topology:subscribe capability (treating its absence as an authorization failure) so subscription privilege is separable from snapshot read; Anchor Nodes that do not yet enforce topology:subscribe MUST at minimum enforce topology:read. Nodes that cannot enforce topology:subscribe MUST document non-enforcement explicitly in the NWM stability metadata.The IdentFrame is signed by the requester’s private key, so the claim is integrity-protected but self-declared — it is not CA-attested at Phase 1–2.
AnnounceFrame (within TTL) declares node_roles containing "anchor". A mismatch SHOULD produce NWP-TOPOLOGY-UNAUTHORIZED with a hint. An absent AnnounceFrame MUST NOT block a requester that has passed the capability gate.topology.stream subscription, if the Anchor revokes the requester’s capability set (e.g. RevokeFrame received from the CA, NID expiry, or scope narrowing) the server MUST emit a terminal NWP-TOPOLOGY-UNAUTHORIZED event on the stream and then close the stream. The event carries the standard DiffFrame envelope with event_type = "error" and a payload of { "code": "NWP-TOPOLOGY-UNAUTHORIZED", "reason": "<revoked | expired | scope_narrowed>" }. Anchor Nodes MUST NOT silently drop subscribers — a clean rejection event is required so clients can distinguish authorization loss from transport-level disconnects.reputation_policy): When the receiving Anchor declares a reputation_policy (NWM Phase 2 field, see NPS-RFC-0004 §4.4) and the requesting NID’s reputation score drops below a configured threshold while a topology.stream subscription is active, the Anchor SHOULD emit a terminal event with payload.code = "NWP-AUTH-REPUTATION-BLOCKED" carrying the matching incident, severity, and ledger entry seq (per error code §14), then close the stream. For initial-handshake reputation rejection (request time), the standard synchronous NWP-AUTH-REPUTATION-BLOCKED error code applies and the subscription is never opened. Anchors without a reputation_policy declared have no obligation to evaluate reputation.id-nps-node-roles cert extension (to be defined in a follow-up RFC-0002 amendment) to close the self-declaration gap and bind the role claim to a CA-issued certificate.Fine-grained per-cluster namespace or ACL policies remain implementation-defined and are tracked for a follow-up CR.
npsd exposes a WebSocket endpoint for browser clients is tracked separately. Topology query semantics here are transport-independent.SubscribeFrame opens a server-push subscription on a Memory or Anchor Node. The server streams matching events as DiffFrame messages until the subscription is closed.
| Field | Type | Required | Description |
|---|---|---|---|
frame |
uint8 | Required | Fixed value 0x12 |
subscription_id |
string | Required | Client-generated UUID v4; used to correlate events and cancel the subscription |
operation |
string | Optional | open (default), renew, or close. renew and close address an existing subscription_id and MUST omit target/filter fields. |
type |
string | Optional | Reserved subscribe type identifier per §12. When set, type-specific fields apply and anchor_ref semantics are defined by the type. Absent: per-anchor subscribe behavior below |
anchor_ref |
string | Conditionally Required | anchor_id of the subscribed data. Required for default per-anchor subscriptions; omitted when a reserved type defines its own target semantics (for example topology.stream) |
filter |
object | Optional | Same filter syntax as QueryFrame filter (§6); if absent, all events match |
heartbeat_interval_ms |
uint32 | Optional | If set, server MUST emit a heartbeat DiffFrame (empty payload, event_type = "heartbeat") at this interval; default 0 (no heartbeat) |
max_events |
uint32 | Optional | Server closes the subscription after delivering this many events; 0 = unlimited |
cursor |
string | Optional | Resume from a prior position; if the cursor is expired the server MUST return NWP-SUBSCRIBE-SEQ-TOO-OLD |
lease_seconds |
uint32 | Optional | Requested lease for open or renew. Valid only when subscription_policy is advertised; the server clamps it to max_lease_seconds. Zero is invalid. |
subscription_id echoed, status = "open")subscription_id in an EXT header or equivalent transport metadatamax_events is reachedNWP-SUBSCRIBE-* code before closingSubscription-pushed DiffFrames add the following event-envelope fields to the standard DiffFrame fields:
| Field | Type | Required | Description |
|---|---|---|---|
subscription_id |
string | Required | Associated subscription ID |
seq |
uint64 | Required except terminal resync_required |
Monotonically increasing event sequence number within the subscription; reserved subscribe types MAY bind this to a domain-specific version, such as topology version (§12.3) |
event_type |
string | Required | "create" / "update" / "delete" / "heartbeat" / "error" for default subscriptions. Reserved subscribe types (§12) MAY define additional values |
timestamp |
string | Optional | Time of change (ISO 8601) |
payload |
object | Optional | Event-specific payload. Heartbeats use an empty payload |
cgn_est |
uint32 | Optional | Estimated CGN token cost of this push event’s payload. Nodes SHOULD populate this field on each pushed DiffFrame so subscribers can perform Agent-side cumulative-budget accounting per token-budget.md §7.2. Absent means the node does not provide a per-event estimate; Agents MAY estimate locally via UTF-8/4 fallback |
Cursor semantics
seq gap, it SHOULD re-subscribe with the latest server-issued cursor it has received.cursor is outside the retention window, the node MUST return NWP-SUBSCRIBE-SEQ-TOO-OLD or emit a reserved-type-specific terminal resync event (for example topology.stream resync_required).The following error codes (defined in §14) apply to SubscribeFrame operations:
NWP-SUBSCRIBE-STREAM-NOT-FOUND — subscription_id unknown or already closedNWP-SUBSCRIBE-LIMIT-EXCEEDED — server’s concurrent subscription limit reachedNWP-SUBSCRIBE-FILTER-UNSUPPORTED — filter expression not supported by this nodeNWP-SUBSCRIBE-INTERRUPTED — server-side interruptionNWP-SUBSCRIBE-SEQ-TOO-OLD — cursor position is no longer availablesubscription_policy. Its default and maximum MUST be
positive, and renew_before_seconds MUST be less than the maximum.operation absent means open. The server applies
the requested lease or default_lease_seconds, clamps it to the maximum, and
returns lease_seconds plus RFC 3339 expires_at in the opening CapsFrame.type captured at open time.
A different principal receives the existing authorization error and learns
no subscription state.accepted_at + effective_lease_seconds and returns the new
deadline. It never rewinds a cursor or resets event seq.now >= expires_at, the server atomically closes
the subscription, emits at most one terminal event carrying
NWP-SUBSCRIBE-LEASE-EXPIRED when the transport is writable, and releases
resources. Renew after expiry returns the same error and cannot resurrect it.NWP-SUBSCRIBE-LEASE-INVALID without mutating the subscription.| Error Code | NPS Status Code | Description |
|---|---|---|
NWP-AUTH-NID-SCOPE-VIOLATION |
NPS-AUTH-FORBIDDEN |
Agent scope does not cover the target node |
NWP-AUTH-NID-EXPIRED |
NPS-AUTH-UNAUTHENTICATED |
NID certificate has expired |
NWP-AUTH-NID-REVOKED |
NPS-AUTH-UNAUTHENTICATED |
NID has been revoked |
NWP-AUTH-NID-UNTRUSTED-ISSUER |
NPS-AUTH-UNAUTHENTICATED |
NID issuer not in trusted_issuers |
NWP-AUTH-NID-CAPABILITY-MISSING |
NPS-AUTH-FORBIDDEN |
Agent lacks a capability required by the node |
NWP-AUTH-ASSURANCE-TOO-LOW |
NPS-AUTH-FORBIDDEN |
Agent’s assurance_level is below the node’s min_assurance_level (or the per-action override). Response SHOULD include a hint pointing to a CA enrolment URL. (NPS-RFC-0003) |
NWP-AUTH-REPUTATION-BLOCKED |
NPS-AUTH-FORBIDDEN |
The receiving Node’s reputation_policy (Phase 2 NWM field — see NPS-RFC-0004 §4.4) matched a reject_on rule against the requesting subject_nid. Response SHOULD include the matching incident + severity + log entry seq for traceability. The field shape that produces this error lands at NWP v0.13 (Phase 2); the error code itself is reserved at NWP v0.13 (Phase 1) so SDKs can begin recognising it without round-tripping through a future spec bump. (NPS-RFC-0004) |
NWP-QUERY-FILTER-INVALID |
NPS-CLIENT-BAD-PARAM |
Filter syntax is invalid or nesting is too deep |
NWP-QUERY-FIELD-UNKNOWN |
NPS-CLIENT-BAD-PARAM |
fields references a non-existent field |
NWP-QUERY-CURSOR-INVALID |
NPS-CLIENT-BAD-PARAM |
cursor value cannot be decoded or has expired |
NWP-QUERY-REGEX-UNSAFE |
NPS-CLIENT-BAD-PARAM |
$regex pattern rejected (ReDoS risk or too long) |
NWP-QUERY-VECTOR-UNSUPPORTED |
NPS-SERVER-UNSUPPORTED |
Node does not support vector search |
NWP-QUERY-AGGREGATE-UNSUPPORTED |
NPS-SERVER-UNSUPPORTED |
Node does not support aggregation queries |
NWP-QUERY-AGGREGATE-INVALID |
NPS-CLIENT-BAD-PARAM |
aggregate structure is invalid (unknown func, duplicate alias, etc.) |
NWP-QUERY-STREAM-UNSUPPORTED |
NPS-SERVER-UNSUPPORTED |
Node does not support streaming queries |
NWP-ACTION-NOT-FOUND |
NPS-CLIENT-NOT-FOUND |
action_id does not exist |
NWP-ACTION-PARAMS-INVALID |
NPS-CLIENT-UNPROCESSABLE |
Operation parameter schema validation failed |
NWP-ACTION-IDEMPOTENCY-CONFLICT |
NPS-CLIENT-CONFLICT |
A request with the same idempotency_key is already in progress |
NWP-LLM-CONTEXT-NOT-FOUND |
NPS-CLIENT-NOT-FOUND |
Stateful LLM context is unknown/released, or idempotency lookup expired (§7.6) |
NWP-LLM-CONTEXT-EXPIRED |
NPS-CLIENT-GONE |
Stateful LLM context has an idle-expiry tombstone (§7.6) |
NWP-LLM-CONTEXT-VERSION-CONFLICT |
NPS-CLIENT-CONFLICT |
base_version is stale or a concurrent mutation owns the reservation (§7.6) |
NWP-LLM-CONTEXT-BINDING-MISMATCH |
NPS-CLIENT-CONFLICT |
Model/system/tools/runtime binding differs from the committed context (§7.6) |
NWP-LLM-CONTEXT-FORBIDDEN |
NPS-AUTH-FORBIDDEN |
Caller is not the context owner or lacks scope/capability (§7.6) |
NWP-LLM-CONTEXT-LIMIT-EXCEEDED |
NPS-LIMIT-RESOURCE |
Per-principal live-context limit reached (§7.6) |
NWP-LLM-CONTEXT-OPERATION-UNSUPPORTED |
NPS-SERVER-UNSUPPORTED |
Context is supported but the requested lifecycle operation is not (§7.6) |
NWP-TASK-NOT-FOUND |
NPS-CLIENT-NOT-FOUND |
task_id does not exist |
NWP-TASK-ALREADY-CANCELLED |
NPS-CLIENT-CONFLICT |
Task has already been cancelled |
NWP-TASK-ALREADY-COMPLETED |
NPS-CLIENT-CONFLICT |
Task has already completed; cannot cancel |
NWP-TASK-ALREADY-FAILED |
NPS-CLIENT-CONFLICT |
Task has already failed; cannot cancel |
NWP-SUBSCRIBE-STREAM-NOT-FOUND |
NPS-CLIENT-NOT-FOUND |
subscription_id referenced by a subscription operation does not exist or is already closed |
NWP-SUBSCRIBE-LIMIT-EXCEEDED |
NPS-LIMIT-EXCEEDED |
Exceeded maximum concurrent subscriptions |
NWP-SUBSCRIBE-FILTER-UNSUPPORTED |
NPS-SERVER-UNSUPPORTED |
Node does not support filtered subscriptions |
NWP-SUBSCRIBE-INTERRUPTED |
NPS-SERVER-UNAVAILABLE |
Subscription stream terminated due to underlying data source interruption |
NWP-SUBSCRIBE-SEQ-TOO-OLD |
NPS-CLIENT-CONFLICT |
cursor is outside the node’s retention window; full re-query or reserved-type resync required |
NWP-SUBSCRIBE-LEASE-INVALID |
NPS-CLIENT-BAD-PARAM |
Renewable-subscription operation or lease/policy bounds are invalid |
NWP-SUBSCRIBE-LEASE-EXPIRED |
NPS-CLIENT-GONE |
Subscription lease reached its deadline and cannot be renewed or resurrected |
NWP-BUDGET-EXCEEDED |
NPS-LIMIT-BUDGET |
Response would exceed the token budget |
NWP-DEPTH-EXCEEDED |
NPS-CLIENT-BAD-PARAM |
depth exceeds the node’s allowed max_depth |
NWP-GRAPH-CYCLE |
NPS-CLIENT-UNPROCESSABLE |
Node graph contains a circular reference |
NWP-NODE-UNAVAILABLE |
NPS-SERVER-UNAVAILABLE |
Underlying data source temporarily unavailable |
NWP-MANIFEST-VERSION-UNSUPPORTED |
NPS-CLIENT-BAD-PARAM |
Agent NPS version is below the node’s min_agent_version |
NWP-MANIFEST-NODE-TYPE-REMOVED |
NPS-CLIENT-BAD-FRAME |
NWM node_type contains the removed legacy value "gateway" (NPS-CR-0001); response SHOULD include a hint pointing to NPS-CR-0001 |
NWP-MANIFEST-NODE-TYPE-UNKNOWN |
NPS-CLIENT-BAD-FRAME |
NWM node_type contains an unrecognized value (see NWP-MANIFEST-NODE-TYPE-REMOVED for the "gateway" case) |
NWP-RATE-LIMIT-EXCEEDED |
NPS-LIMIT-RATE |
Rate limit exceeded |
NWP-RESERVED-TYPE-UNSUPPORTED |
NPS-SERVER-UNSUPPORTED |
QueryFrame or SubscribeFrame type is an unrecognized reserved-type identifier (§12). Distinct from NWP-ACTION-NOT-FOUND — the unknown operand is type, not action_id. |
NWP-HTTP-ORIGIN-FORBIDDEN |
NPS-AUTH-FORBIDDEN |
HTTP overlay origin policy rejected the caller (§9.5) |
NWP-HTTP-CONTENT-TYPE-UNSUPPORTED |
NPS-CLIENT-BAD-FRAME |
HTTP overlay request Content-Type is not a supported NWP frame media type (§9.5) |
NWP-HTTP-ACCEPT-UNSATISFIABLE |
NPS-CLIENT-BAD-PARAM |
HTTP overlay request Accept cannot be satisfied by any supported response media type (§9.5) |
NWP-HTTP-REQUEST-ID-MISMATCH |
NPS-CLIENT-BAD-PARAM |
Response X-NWP-Request-ID does not echo the request ID (§9.5) |
NWP-HTTP-FRAME-BODY-MALFORMED |
NPS-CLIENT-BAD-FRAME |
HTTP body cannot be parsed as a supported NWP frame envelope (§9.5) |
NWP-HTTP-BODY-TOO-LARGE |
NPS-LIMIT-PAYLOAD |
HTTP request body exceeds the server’s NWP body limit (§9.5, §16.5.1) |
NWP-CAPABILITY-ADVERTISED-UNIMPLEMENTED |
NPS-SERVER-UNSUPPORTED |
NWM advertises a capability/profile that the node currently cannot serve (§9.5) |
NWP-TOPOLOGY-UNAUTHORIZED |
NPS-AUTH-FORBIDDEN |
Caller lacks permission to read this Anchor’s topology (§12). Authorization policy is implementation-defined per §12.4 |
NWP-TOPOLOGY-UNSUPPORTED-SCOPE |
NPS-CLIENT-BAD-PARAM |
topology.scope value is not implemented by this Anchor |
NWP-TOPOLOGY-DEPTH-UNSUPPORTED |
NPS-CLIENT-BAD-PARAM |
Requested topology.depth exceeds this Anchor’s maximum |
NWP-TOPOLOGY-FILTER-UNSUPPORTED |
NPS-CLIENT-BAD-PARAM |
topology.filter contains an unrecognized key |
NWP-ANCHOR-NOT-LEADER |
NPS-CLIENT-CONFLICT |
A topology-mutating write reached a standby (or read-only-degraded) Anchor; only the active owner of the current cluster_epoch may accept writes (§12.2, NPS-CR-0009) |
NWP-ANCHOR-EPOCH-FENCED |
NPS-CLIENT-CONFLICT |
An inbound frame carries a cluster_epoch higher than the receiving Anchor’s own; the receiver is a superseded leader and fences itself (§12.2, NPS-CR-0009) |
NWP-BRIDGE-DIRECTION-UNSUPPORTED |
NPS-SERVER-UNSUPPORTED |
The request arrived on a direction/protocol this Bridge Node does not declare — see §16.2; the response SHOULD carry both bridge_protocols and bridge_inbound_protocols in hint (NPS-CR-0010) |
Nodes MUST validate the Agent NID’s scope on every request. Requests exceeding scope MUST return NWP-AUTH-NID-SCOPE-VIOLATION and MUST NOT return any data.
When Complex Nodes resolve sub-node references, they MUST maintain an allowlist of permitted node URL prefixes and MUST NOT access internal network addresses (RFC 1918).
When the budget is exceeded, nodes SHOULD prefer trimming response content (field reduction → summary → record truncation); if trimming is not possible, they MUST return NWP-BUDGET-EXCEEDED and MUST NOT silently truncate data. See token-budget.md §4.3.
Nodes SHOULD enforce per-Agent NID rate limiting. On limit exceeded, return NWP-RATE-LIMIT-EXCEEDED with an X-NWP-Rate-Reset header. Unauthenticated requests SHOULD be limited by IP.
$regex patterns MUST undergo ReDoS detection; filter nesting depth ≤ 8callback_url MUST use the https:// schemeCOMPLETED rather than retrying indefinitelyAnchor Nodes implementing §12 MUST treat topology.snapshot and topology.stream as authenticated surfaces. The minimum authorization binding is defined in §12.4: at Phase 1–2, the requesting NID MUST declare topology:read in IdentFrame.capabilities (primary gate); NDP node_roles cross-check is a defense-in-depth SHOULD. Unauthorized callers MUST receive NWP-TOPOLOGY-UNAUTHORIZED rather than silent empty responses to prevent oracle attacks against cluster membership.
The Bridge Node type and the bridge_target object schema were introduced by NPS-CR-0001 (§2.1)
and standardized at NWP v0.13. NPS-CR-0010 settled Bridge
Node as bidirectional and split this section into two independent conformance profiles. This
section formalises both, provides the normative error mapping shared by them, and provides canonical
bridge_target round-trip test vectors so that all SDK Bridge implementations agree on the wire shape.
A Bridge Node MUST claim at least one of the two profiles below and MUST declare it on the wire (§2.1, NPS-4 §3.1). Claiming neither is not a conformant Bridge Node. A Bridge Node MAY claim both. An implementation that claims only Outbound — the only profile that existed through alpha.15 — remains fully conformant with no change.
Claimed by declaring a non-empty bridge_protocols. A conformant outbound Bridge Node MUST:
node_type: "bridge" in its NWM (§4.1) and the supported external protocols via
NDP bridge_protocols (NPS-4 §3.1).bridge_target object and reject a frame missing it, or
one that fails bridge_target schema validation, with NWP-BRIDGE-TARGET-INVALID.bridge_target.protocol against its advertised set; a protocol with no registered
dispatcher MUST return NWP-BRIDGE-PROTOCOL-UNSUPPORTED (not a silent fallthrough).bridge_target fields as opaque pass-through and MUST NOT fail on them
(forward compatibility).topology.* MUST
return NWP-RESERVED-TYPE-UNSUPPORTED on a pure Bridge Node).A conformant outbound Bridge Node SHOULD:
bridge_target.endpoint (NPS-2 §15.2) before dialing the upstream.bridge_target.extras.headers to the upstream HTTP request verbatim, minus
hop-by-hop headers.Claimed by declaring a non-empty bridge_inbound_protocols. A conformant inbound Bridge Node MUST:
node_type: "bridge" in its NWM (§4.1) and the served protocols via NDP
bridge_inbound_protocols (NPS-4 §3.1).initialize, ping, tools/list, tools/call, resources/list, and resources/read —
an inbound MCP Bridge that omits resources/* is not conformant.bridge_inbound_protocols with
NWP-BRIDGE-DIRECTION-UNSUPPORTED; the response SHOULD carry both declared arrays in hint.A conformant inbound Bridge Node SHOULD:
bridge_protocols and bridge_inbound_protocols are independent sets over the same value domain
("http", "grpc", "mcp", "a2a"). A protocol MAY appear in both (bridged in both directions).
A node declaring node_roles: ["bridge"] MUST have at least one of the two non-empty. Receivers MUST
treat an absent bridge_inbound_protocols as [] — which is exactly a pre-alpha.16, outbound-only
Bridge Node. (NPS-CR-0010 §3.2)
Both directions translate NPS status codes into (or out of) a foreign protocol’s error space. Through
alpha.15 this mapping was implemented twice per protocol — once in the outbound dispatcher, once in
the then-separate compat/*-ingress package — and the two copies drifted. The mapping is therefore
normative, and a single implementation of it MUST serve both directions.
MCP (JSON-RPC 2.0). NPS status → JSON-RPC error code:
| NPS status | JSON-RPC code | Notes |
|---|---|---|
NPS-CLIENT-BAD-FRAME |
-32600 (Invalid Request) |
|
NPS-CLIENT-BAD-PARAM, NPS-CLIENT-UNPROCESSABLE |
-32602 (Invalid params) |
|
NPS-CLIENT-NOT-FOUND |
-32601 (Method not found) for an unknown tool in tools/call; -32602 for an unknown URI in resources/read |
The distinction matters: an unknown tool is a missing method to an MCP client, an unknown resource is a bad argument |
NPS-CLIENT-GONE |
-32602 |
|
NPS-CLIENT-CONFLICT |
-32004 (implementation-defined) |
|
NPS-AUTH-UNAUTHENTICATED |
-32001 (implementation-defined) |
MUST be a JSON-RPC error, never a successful result carrying an error payload |
NPS-AUTH-FORBIDDEN |
-32003 (implementation-defined) |
MUST NOT be collapsed onto -32001 |
NPS-LIMIT-RATE, NPS-LIMIT-BUDGET, NPS-LIMIT-PAYLOAD, NPS-LIMIT-RESOURCE |
-32005 (implementation-defined) |
|
NPS-SERVER-UNSUPPORTED |
-32601 (Method not found) |
Includes NWP-BRIDGE-DIRECTION-UNSUPPORTED |
NPS-SERVER-INTERNAL, NPS-SERVER-UNAVAILABLE, NPS-SERVER-TIMEOUT, NPS-DOWNSTREAM-UNAVAILABLE |
-32603 (Internal error) |
Upstream node failure |
| parse failure before dispatch | -32700 (Parse error) |
The application-defined code -32002 is reserved and MUST NOT be emitted. Through alpha.15 the
.NET Bridge used it for “tool not found”; NPS-CR-0010 maps an unknown tool to -32601
(Method not found) instead — which is what an MCP client already understands — and leaves -32002
reserved rather than reassigning it, so a client pinned to the old behaviour cannot silently misread
some other error as a missing tool.
The reverse direction (JSON-RPC error → NPS status) is the inverse of this table; where the inverse is
not injective, an implementation MUST choose the most specific NPS status, never a generic
NPS-SERVER-INTERNAL.
gRPC. NPS status → gRPC status code:
| NPS status | gRPC status |
|---|---|
NPS-CLIENT-BAD-FRAME, NPS-CLIENT-BAD-PARAM, NPS-CLIENT-UNPROCESSABLE |
INVALID_ARGUMENT |
NPS-CLIENT-NOT-FOUND, NPS-CLIENT-GONE |
NOT_FOUND |
NPS-CLIENT-CONFLICT |
ABORTED |
NPS-AUTH-UNAUTHENTICATED |
UNAUTHENTICATED |
NPS-AUTH-FORBIDDEN |
PERMISSION_DENIED |
NPS-LIMIT-RATE, NPS-LIMIT-BUDGET, NPS-LIMIT-PAYLOAD, NPS-LIMIT-RESOURCE |
RESOURCE_EXHAUSTED |
NPS-SERVER-UNSUPPORTED |
UNIMPLEMENTED |
NPS-SERVER-INTERNAL |
INTERNAL |
NPS-SERVER-UNAVAILABLE, NPS-DOWNSTREAM-UNAVAILABLE |
UNAVAILABLE |
NPS-SERVER-TIMEOUT |
DEADLINE_EXCEEDED |
A2A. NPS status → A2A task state: client-class errors terminate the task as failed with the NPS
code preserved verbatim in the failure detail; server-class errors terminate as failed and are
retryable. An A2A Bridge MUST NOT silently downgrade an error to a completed task.
An implementation MUST NOT collapse distinct NPS status classes onto a single foreign error code (§16.1.2 SHOULD-2 makes this observable); doing so is a conformance failure.
bridge_target test vectorsThe canonical wire shape is { "protocol", "endpoint", "extras"? } (the SDK in-memory form;
headers and other per-protocol knobs travel inside extras). All six SDKs MUST round-trip
these vectors identically (from_dict(to_dict(x)) == x):
{ "protocol": "http", "endpoint": "https://api.example.com/v1/orders" }
{
"protocol": "http",
"endpoint": "https://api.example.com/v1/orders",
"extras": { "method": "POST", "headers": { "X-Tenant": "acme" } }
}
{ "protocol": "grpc", "endpoint": "grpc.example.com:443/orders.OrderService/Create" }
{ "protocol": "mcp", "endpoint": "https://mcp.example.com/sse", "extras": { "tool": "create_order" } }
Vector rules:
protocol and endpoint are required and MUST be preserved verbatim.extras MUST be omitted from the serialized form when empty/absent (not emitted as null).BridgeNodeDescriptor serialises supported_protocols as a sorted array for stable
output across SDKs.NWP v0.20 defines a transport-independent decision profile for SDK-hosted servers. It does not add
a frame type. Implementations MAY expose framework-specific middleware, but their admission,
dispatch, cancellation, and error decisions MUST match the shared vectors in
spec/conformance/nwp/.
Memory, Action, and Complex Node servers claiming the portable profile MUST:
/.nwm with application/nwp-manifest+json and require GET; a method rejection MUST
return HTTP 405 with Allow: GET. Memory and Complex Nodes MUST dispatch QueryFrame; Action
and Complex Nodes MUST dispatch ActionFrame.POST for /query and /invoke. A method rejection occurs before frame admission,
returns HTTP 405, and MUST include Allow: POST.application/nwp-frame. During the alpha.17 compatibility window they MUST also accept
legacy request media type application/x-nps-frame, but MUST emit only the canonical
application/nwp-capsule, application/nwp-error+json, and
application/nwp-manifest+json response media types. The legacy alias is deprecated and is
removed from the required profile in alpha.18.NPS-LIMIT-PAYLOAD / NWP-HTTP-BODY-TOO-LARGE.Content-Type, unsatisfied Accept, and malformed
bodies. HTTP error bodies MUST use application/nwp-error+json.X-NWP-Request-ID in HTTP mode and the
frame request_id in native mode. Responses that have a correlation field MUST echo it.success, rejected, cancelled, or timeout; the
correlation identifier SHOULD be attached when the telemetry system permits it.In native mode, unsupported decoded frame types MUST produce an ErrorFrame with
NPS-CLIENT-BAD-FRAME / NWP-NATIVE-FRAME-UNSUPPORTED. A provider failure after successful
admission remains NWP-NATIVE-DISPATCH-FAILED unless a more specific protocol error is available.
The normative cases are portable_node_server_vectors.json.
An outbound Bridge server claiming the portable profile MUST perform the following checks before dialing an upstream:
bridge_target shape, then resolve protocol in the registered dispatcher set.
A missing protocol is NPS-CLIENT-UNPROCESSABLE / NWP-BRIDGE-TARGET-INVALID; a well-formed but
unregistered protocol is NPS-SERVER-UNSUPPORTED /
NWP-BRIDGE-PROTOCOL-UNSUPPORTED.NPS-CLIENT-UNPROCESSABLE /
NWP-BRIDGE-ENDPOINT-INVALID. SDK policy evaluation MUST be deterministic and MUST NOT require
DNS for literal-address inputs; hosts resolved at dial time remain subject to rebinding-safe
address checks.NPS-SERVER-TIMEOUT /
NWP-BRIDGE-UPSTREAM-FAILED; cancellation takes precedence over timeout and produces no
synthesized response.NPS-OK-ACCEPTED.The normative cases are bridge_lifecycle_vectors.json. All six reference SDKs MUST execute both
v0.20 vector sets in CI. Equivalent decisions, status/error pairs, response media types, and
correlation behavior are required across HTTP and native hosts.
| Version | Date | Changes |
|---|---|---|
| 0.22 | 2026-08-31 | Alpha.19 NWM/subscription hardening: deterministic stability/SLA/billing normalization and action fallback; advertised renewable subscription policy; open/renew/close operations with principal binding, clamped leases, explicit deadlines, non-resurrectable expiry, and shared conformance vectors. Adds two subscription errors. Depends-On advances to NCP 0.12, NIP 0.15, and NDP 0.13. |
| 0.21 | 2026-08-12 | NPS-CR-0011 stateful LLM context/delta completion: NWM LLM profile 0.2 advertises explicit context operations/persistence/limits; llm.complete gains optional owner-bound opaque context requests and terminal receipts; adds status/release lifecycle actions, CAS versions, binding checks, atomic cancellation/stream commit, restart/idempotency semantics, measured wire_input_bytes, seven deterministic errors, and shared conformance vectors. Stateless completion remains compatible and stateful requests never silently fall back. Depends-On NIP advanced to v0.14 for llm:context. No new frame type. |
| 0.20 | 2026-07-29 | Added §16.5 portable Node/Bridge server profile and shared cross-language vectors. Standardized HTTP/native admission, role dispatch, canonical/legacy MIME handling for the alpha.17 compatibility window, finite body limits, cancellation, correlation propagation, Bridge dispatcher/SSRF/deadline preflight, async task mode, and terminal telemetry outcomes. Added NWP-HTTP-BODY-TOO-LARGE → NPS-LIMIT-PAYLOAD; no new frame type. Depends-On advanced to NCP v0.11 and NIP v0.13. |
| 0.19 | 2026-07-23 | NPS-CR-0010 Bridge Node is bidirectional: resolved the spec’s own contradiction — the §2.1 taxonomy, the “Removed types” note, and NPS-CR-0001 all defined Bridge Node as NPS↔non-NPS translation, while the §2.1 callout and the normative MUST list narrowed it to NPS→external only. The narrowing existed solely to keep the name Bridge distinct from the then-separate compat/*-ingress packages; those are now absorbed into the Bridge package and the restriction is lifted. Bridge Node semantics restructured into Outbound (unchanged) + Inbound (new) MUST lists; MCP inbound MUST serve resources/* as well as tools/*. §16 split into two independent conformance profiles (§16.1.1 outbound / §16.1.2 inbound), a normative direction declaration (§16.2), and normative per-protocol error-mapping tables (§16.3) that a single implementation MUST serve in both directions. Role-vs-library boundary made explicit: only a deployment announcing node_roles: ["bridge"] is a Bridge Node. Depends-On NDP bumped to v0.11 (defines bridge_inbound_protocols). One new error code NWP-BRIDGE-DIRECTION-UNSUPPORTED. Additive and backward-compatible: an outbound-only Bridge Node remains conformant unchanged. (Renumbered from the edge-line 0.16 — the released alpha.16 line had independently used 0.15–0.17 for the LLM profile series below.) |
| 0.18 | 2026-07-23 | NPS-CR-0009 multi-Anchor HA: finalised the two §12.2 anchor_state sub-types anchor_failover (successor_nid / cluster_epoch / reason) and anchor_quorum_lost (quorum_size / available), removing the Phase-3-placeholder “MUST NOT emit” restriction. New cluster_epoch (uint64) ownership fence on topology responses/writes; standby writes rejected with NWP-ANCHOR-NOT-LEADER, superseded leaders fenced with NWP-ANCHOR-EPOCH-FENCED. Additive and Phase-gated: single-Anchor clusters keep cluster_epoch = 1 and are unaffected. Depends-On NDP bumped to v0.10 (defines cluster_epoch on AnnounceFrame + highest-epoch resolution). Two new error codes. |
| 0.17 | 2026-07-05 | Added §9.5 HTTP Binding Rejection Codes and six canonical NWP error codes for HTTP overlay preconditions and advertised-but-unimplemented capability rollout windows: NWP-HTTP-ORIGIN-FORBIDDEN, NWP-HTTP-CONTENT-TYPE-UNSUPPORTED, NWP-HTTP-ACCEPT-UNSATISFIABLE, NWP-HTTP-REQUEST-ID-MISMATCH, NWP-HTTP-FRAME-BODY-MALFORMED, and NWP-CAPABILITY-ADVERTISED-UNIMPLEMENTED. Shared error-codes.md bumped to v1.6. |
| 0.16 | 2026-07-04 | Adds §4.2a NWM profiles and the standard LLM/Thinking Profile (profiles.llm) for model-serving Action/Complex Nodes. Clarifies that “Thinking Node” is a product-facing alias, not a new node_type; coarse discovery uses NIP/NDP llm:* capabilities while detailed model, streaming, tool, privacy, and reasoning-disclosure metadata lives in NWM. No new frame type or error code. Depends-On NIP bumped to v0.11 for the llm:* capability registry. |
| 0.15 | 2026-07-04 | New §7.5 standardizes the llm.complete ActionFrame contract: typed request/response DTO shape, stop_reason enum, tool call field names, sync/async/streaming response semantics, ErrorFrame-vs-payload-error rule, and snake_case JSON/MessagePack key policy. No new frame type or error code. |
| 0.14 | 2026-06-12 | New §16 Bridge Node Conformance: formal MUST/SHOULD requirements (advertise node_type: "bridge", validate bridge_target.protocol, opaque unknown-field pass-through, stateless / no-topology) and canonical bridge_target round-trip test vectors (http / grpc / mcp, with extras) all six SDKs must round-trip identically. §16 Changelog renumbered to §17. No new error codes; no Depends-On change. |
| 0.14 | 2026-06-03 | NWM manifest_version type changed from opaque string (ETag) to uint32 monotonic counter (starts at 1, incremented on every structural change). New NWM field manifest_updated_at (ISO 8601, optional) records the last-change timestamp. Servers MUST return X-NWM-Version: <manifest_version> on every GET /.nwm response; agents use If-None-Match: <uint32> for conditional requests. No new error codes; 304 Not Modified reused for cache hits. |
| 0.13 | 2026-05-28 | §13 SubscribeFrame (0x12) formal specification (closes CR-0006): field table (subscription_id, filter, heartbeat_interval_ms, max_events, cursor), lifecycle (open→active→heartbeat→close), error code reference. §12.4 topology:subscribe enforcement promoted SHOULD → MUST; nodes that cannot enforce MUST document non-enforcement in NWM stability metadata. NWM gains optional trust_anchors field (array of CA NID URNs). BridgeNode bridge_target object schema standardized (protocol + endpoint + extras carrier). |
| 0.12 | 2026-05-11 | NPS-CR-0002 Phase 2 spec gaps closed. §8.2 DiffFrame extension table gains optional cgn_est field (uint32) for per-event CGN reporting on push streams per token-budget.md §7.2; columns reformatted to include Required. §12.2 topology.stream events table: anchor_state row gains explicit sub-type discriminator schema (version_rebased defined for Phase 1–2; anchor_failover and anchor_quorum_lost reserved as Phase 3 placeholder slots — implementations MUST NOT emit Phase 3 sub-types pre-stable and MUST ignore unknown sub-types for forward compatibility); resync_required trigger and reason enum broadened (version_too_old / anchor_rebased / server_state_lost). §12.4 Phase 1–2 authorization model expanded: (a) capability gate split per surface — topology.snapshot requires topology:read; topology.stream requires topology:read AND SHOULD additionally require topology:subscribe in Phase 2 (MUST in Phase 3); (b) new mid-stream rejection rule — server MUST emit terminal NWP-TOPOLOGY-UNAUTHORIZED event then close the stream on capability revocation; (c) new reputation interaction — for active subscriptions, Anchors with a declared reputation_policy SHOULD emit terminal NWP-AUTH-REPUTATION-BLOCKED and close the stream when the subscriber’s reputation drops below threshold. No new error codes; existing NWP-TOPOLOGY-UNAUTHORIZED and NWP-AUTH-REPUTATION-BLOCKED reused. No Depends-On change. See issue #41. |
| 0.11 | 2026-05-10 | NWM gains optional top-level stability (experimental/stable/deprecated), sla (object: p95_latency_ms, availability, sla_tier), and billing (object: metering_profile, billing_unit, price_hint, currency) fields (§4.1, §4.4a, §4.4b). ActionSpec (§4.6) gains matching per-action stability / sla / billing overrides with field-level fallback to the top-level values. All fields are advisory (no protocol-level enforcement) and backward-compatible — pre-0.11 manifests are treated as stability="stable" with no SLO/billing metadata. Enables marketplace / NeuronHub clients to filter, warn, or rank services by lifecycle stage and commercial profile per AaaS-Profile discovery requirements. No new error codes; no Depends-On change. See issue #36. |
| 0.10 | 2026-05-01 | §12.4 authorization model replaced “implementation-defined” with a Phase 1–2 minimum binding: Anchor Nodes MUST require topology:read in IdentFrame.capabilities (capability gate, self-declared but signed); SHOULD cross-check NDP node_roles contains "anchor" as defense-in-depth; Phase 3 [RFC-0002 stable] adds CA-attested id-nps-node-roles cert extension. §14.7 updated to reference §12.4 defined minimum instead of the previous hedging “SHOULD restrict” language. Depends-On NIP bumped to v0.6 (defines topology:read capability). |
| 0.9 | 2026-05-01 | Breaking rename (pre-1.0): Topology member object field node_kind renamed to node_roles (§12.1); topology stream filter key node_kind renamed to node_roles (§12.2). §2.1 updated: node_kind reference to node_roles. New §2.1 Node Role Resolution section: node_roles (NDP, discovery-layer, array) and node_type (NWM, service-layer, string) are distinct fields — node_type MUST be one of the values in node_roles; validators SHOULD verify against cached NDP data. §4.1 node_type description updated with the cross-protocol constraint and pointer to §2.1. §14.7 node_kind reference updated to node_roles. Depends-On NDP bumped to v0.6. Fixes M1 naming-disambiguation issue. |
| 0.8 | 2026-04-27 | New §12 Reserved Query Types introducing the topology.* namespace mandatory at NPS-AaaS Profile L2: topology.snapshot (QueryFrame, type="topology.snapshot") and topology.stream (SubscribeFrame, type="topology.stream"). Both QueryFrame §6.1 and SubscribeFrame §8.1 gain an optional top-level type field for opting into reserved types. DiffFrame §8.2 event_type enum extended via reserved subscribe types — topology.stream adds member_joined / member_left / member_updated / anchor_state / resync_required. Five new error codes: NWP-TOPOLOGY-UNAUTHORIZED, NWP-TOPOLOGY-UNSUPPORTED-SCOPE, NWP-TOPOLOGY-DEPTH-UNSUPPORTED, NWP-TOPOLOGY-FILTER-UNSUPPORTED (table §13). New §14.7 Topology Read-back security section. Existing §12 Error Codes / §13 Security / §14 Changelog renumbered to §13 / §14 / §15 to accommodate the new section. See NPS-CR-0002. |
| 0.7 | 2026-04-26 | Breaking. §2.1 Node Types: Gateway Node removed; replaced by Anchor Node (cluster control plane + NOP routing — inherits the existing role) and Bridge Node (NPS↔non-NPS protocol translation — new). NWM node_type enum updated; legacy "gateway" MUST be rejected. Anchor Node detailed semantics (§2.1 inline) cover member dispatch + optional registry; Bridge Node semantics cover HTTP/gRPC/MCP/A2A target adapters. Depends-On upgraded to NDP v0.8 for the node_kind array form + cluster_anchor + bridge_protocols fields. See NPS-CR-0001. |
| 0.6 | 2026-04-25 | NWM gains optional top-level min_assurance_level field (anonymous / attested / verified), with min_assurance_level per-action override on individual ActionSpecs (§4.6). New error code NWP-AUTH-ASSURANCE-TOO-LOW (NPS-AUTH-FORBIDDEN). Depends-On upgraded to NCP v0.7 (NPS-RFC-0001) and NIP v0.9 (NPS-RFC-0003). See NPS-RFC-0003. |
| 0.4 | 2026-04-14 | §3.2 added /actions sub-path; §4.1 NWM added actions field; §4.2 capabilities added stream_query and aggregate; §4.6 NWM Action Registry (ActionSpec, params_anchor/result_anchor/async/idempotent); QueryFrame §6.1 added stream, aggregate, request_id; §6.6 Streaming Query Protocol (StreamFrame sequence, estimated_total, early termination); §6.7 Aggregation queries (COUNT/SUM/AVG/MIN/MAX/COUNT_DISTINCT, group_by, having); ActionFrame §7.1 added request_id; SubscribeFrame §8.1 added resume_from_seq; §8.2 DiffFrame extension fields (monotonic seq, event_type, timestamp) and reconnection semantics; §9.1/9.2 added X-NWP-Request-ID; §9.4 HTTP mode error response format (application/nwp-error+json); §10 updated complete examples (including error response); §13.6 callback_url abuse prevention security section; 5 new error codes (AGGREGATE-UNSUPPORTED/-INVALID, STREAM-UNSUPPORTED, SUBSCRIBE-SEQ-TOO-OLD, task cancel series) |
| 0.3 | 2026-04-14 | SubscribeFrame (0x12); auto_anchor; Filter $not/$exists/$regex; ActionFrame callback_url/priority; system.task.*; NWM min_agent_version/rate_limits; §14.4/14.5 security sections |
| 0.2 | 2026-04-12 | Unified port 17433; AnchorFrame owned by Node; CGN metering; NPS status code mapping |
| 0.1 | 2026-04-10 | Initial specification |
Attribution: LabAcacia / INNO LOTUS PTY LTD · Apache 2.0