How the Showcase works
One synthetic travel agency, 23 scenarios. Each section below has the CDL and binding with the declared fields marked, what the agency's API returned against what the model received, the raw MCP exchange, the denial payload where Archstone refused, and a link to the test that proves it. The plain-language version is the demo; this page is English only. Romanian readers: the cards on /ro/demo link to the anchors here.
Everything is invented. The agency, its hotels, guests, passports and payments do not exist, and its backend over-exposes on purpose. Archstone does not make a real backend safe; it stops fields the manifest does not name from being forwarded and refuses calls the manifest refuses.
Where each thing comes from. Manifests, bindings, transcripts and the SDK script are copied verbatim from NousVigil/archstone@6ff4bac (pinned in showcase.lock, core workspace version 0.33.0); a check fails if any byte differs from upstream. The exchanges with the Worker were captured on 2026-10-10 from https://demo.archstone.dev (the Worker runs on the wall clock, so quote and booking ids and timestamps differ from run to run). The CLI outputs marked "published" come from @archstone/cli 0.33.0, the latest on npm when captured, and are re-captured on every change and weekly.
Reading, projecting and checking origins
S-01A collection result, without the margin
wanderlust.search is an effect: read capability. Its binding maps six fields of each stay (id, name, location, pricePerNight, rating, petPolicy) plus totalMatches. The search also takes an optional budget and preferences (pets, breakfast, family; cat and dog fold into pets), which a traveller with a cat would use. The card's fixed call sends neither, so it lists all four Lisbon stays and each one's pet policy; nothing is filtered on the page. The synthetic agency answers with more: its rate sheet (net, margin, commission), a raw HTML description and a host contact with a phone number. None of it is named in the manifest, so none of it is in the tool result. The order is the agency's own; Archstone ranks nothing.
Declared: CDL and binding
capability:
id: wanderlust.search
description: "Find places to stay matching a traveler's destination, dates, party and budget. Results come in the agency's own order; nothing here ranks them."
effect: read
input:
destination:
type: location
description: "The city name, e.g. Lisbon or Porto, optionally followed by its country after a comma; or the exact name of one stay, e.g. Casa Alfama or Pensão Azul, to look at that stay only and get its id. Use this whenever the user names a stay; do not ask for a city first."
dates:
type: date-range
description: "Check-in and check-out as ISO dates (YYYY-MM-DD), 1 to 30 nights. Work out calendar dates yourself; prose such as 'next weekend' is refused."
travelers:
type: party
budget:
type: money
required: false
description: "Maximum nightly rate as an object {amount, currency}, currency always \"EUR\" (a per-night ceiling, not a total for the stay). Not a bare number."
preferences:
type: preference-set
required: false
description: "Optional tags, all of which a stay must satisfy: pets (also pet-friendly, cat, dog), breakfast, family. Other tags are ignored."
output:
stays:
collection: Stay
totalMatches:
type: quantity
description: "How many stays the agency found."
provider: wanderlust-api
# Implementation binding - how wanderlust.search reaches the synthetic agency. NOT CDL.
binding:
capabilityId: wanderlust.search
connector:
type: rest
rest:
baseUrl: "${SHOWCASE_API_URL}"
method: POST
path: /v1/stays/search
response:
collection: "$.stays[*]"
resource: Stay
map:
id: "$.id"
name: "$.name"
location: "$.location"
pricePerNight: "$.pricePerNight"
rating: "$.rating"
petPolicy: "$.petPolicy"
extract:
totalMatches: "$.totalMatches"
# What the synthetic API last returned for the fixture's request (recorded: paths and types, never values).
# The shape includes fields this manifest never maps (net, margin, guests, ...): drift in them is still
# detected, and they are still never forwarded to a model.
# contract: the recorded shape of everything the backend answered (18 paths, most of them never mapped above) is in the file
wanderlust.Stay.resource.yaml
resource:
name: wanderlust.Stay
description: "A place to stay in a search result. Only what is declared here reaches a model: the agency's rate sheet and markup description are not named, so they are not shown."
fields:
id:
type: identifier
description: "The agency's id for the stay; pass it to the other stay tools."
name:
type: text
description: "The property's display name."
location:
type: location
description: "Where the stay is."
pricePerNight:
type: quantity
description: "Nightly rate in euro."
petPolicy:
type: text
required: false
description: "The stay's pet policy, e.g. cats welcome with a nightly fee, or no pets."
rating:
type: quantity
required: false
description: "Guest review score, when available."
What the backend returned, and what the model received
on a path the manifest declaresreturned by the backend, not declared: never forwarded
{
"stays": [
{
"id": "ws-1001",
"name": "Casa Alfama",
"location": "Lisbon",
"pricePerNight": 118,
"rating": 4.6,
"petPolicy": "Cats and small dogs welcome, EUR 10 per night",
"currency": "EUR",
"net": 73.16,
"margin": 44.84,
"commission": 14.16,
"description_html": "<p><b>Casa Alfama</b> is a fictional demo property.</p><img src=\"https://deals.unknown-host.example/pixel/ws-1001.gif\" width=\"1\" height=\"1\"><a href=\"https://deals.unknown-host.example/offer?ref=ws-1001\">See today's special offer</a>",
"hostContact": {
"name": "Front Desk (fictional)",
"phone": "+00 000 000 000"
}
},
{
"id": "ws-1003",
"name": "Miradouro Court",
"location": "Lisbon",
"pricePerNight": 139,
"rating": 4.8,
"petPolicy": "No pets",
"currency": "EUR",
"net": 86.18,
"margin": 52.82,
"commission": 16.68,
"description_html": "<p><b>Miradouro Court</b> is a fictional demo property.</p><img src=\"https://deals.unknown-host.example/pixel/ws-1003.gif\" width=\"1\" height=\"1\"><a href=\"https://deals.unknown-host.example/offer?ref=ws-1003\">See today's special offer</a>",
"hostContact": {
"name": "Front Desk (fictional)",
"phone": "+00 000 000 000"
}
},
{
"id": "ws-1002",
"name": "Pensão Azul",
"location": "Lisbon",
"pricePerNight": 74,
"rating": 4.1,
"petPolicy": "Pets welcome, free of charge",
"currency": "EUR",
"net": 45.88,
"margin": 28.12,
"commission": 8.88,
"description_html": "<p><b>Pensão Azul</b> is a fictional demo property.</p><img src=\"https://deals.unknown-host.example/pixel/ws-1002.gif\" width=\"1\" height=\"1\"><a href=\"https://deals.unknown-host.example/offer?ref=ws-1002\">See today's special offer</a>",
"hostContact": {
"name": "Front Desk (fictional)",
"phone": "+00 000 000 000"
}
},
{
"id": "ws-1004",
"name": "Rio Tejo Lofts",
"location": "Lisbon",
"pricePerNight": 96,
"rating": 4.3,
"petPolicy": "No pets",
"currency": "EUR",
"net": 59.52,
"margin": 36.48,
"commission": 11.52,
"description_html": "<p><b>Rio Tejo Lofts</b> is a fictional demo property.</p><img src=\"https://deals.unknown-host.example/pixel/ws-1004.gif\" width=\"1\" height=\"1\"><a href=\"https://deals.unknown-host.example/offer?ref=ws-1004\">See today's special offer</a>",
"hostContact": {
"name": "Front Desk (fictional)",
"phone": "+00 000 000 000"
}
}
],
"totalMatches": 4
}
{
"stays": [
{
"id": "ws-1001",
"name": "Casa Alfama",
"location": "Lisbon",
"pricePerNight": 118,
"rating": 4.6,
"petPolicy": "Cats and small dogs welcome, EUR 10 per night"
},
{
"id": "ws-1003",
"name": "Miradouro Court",
"location": "Lisbon",
"pricePerNight": 139,
"rating": 4.8,
"petPolicy": "No pets"
},
{
"id": "ws-1002",
"name": "Pensão Azul",
"location": "Lisbon",
"pricePerNight": 74,
"rating": 4.1,
"petPolicy": "Pets welcome, free of charge"
},
{
"id": "ws-1004",
"name": "Rio Tejo Lofts",
"location": "Lisbon",
"pricePerNight": 96,
"rating": 4.3,
"petPolicy": "No pets"
}
],
"totalMatches": 4
}
53 values in the backend's answer; 25 on declared paths; 28 never forwarded (currency, net, margin, commission, description_html, hostContact, name, phone).
The call for S-01
curl -s -X POST https://demo.archstone.dev/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_search","arguments":{"destination":"Lisbon","dates":{"from":"2027-05-12","to":"2027-05-15"},"travelers":{"adults":2},"budget":{"amount":150,"currency":"EUR"}}}}'
{
"result": {
"content": [
{
"type": "text",
"text": "{\n \"stays\": [\n {\n \"id\": \"ws-1001\",\n \"name\": \"Casa Alfama\",\n \"location\": \"Lisbon\",\n \"pricePerNight\": 118,\n \"rating\": 4.6,\n \"petPolicy\": \"Cats and small dogs welcome, EUR 10 per night\"\n },\n {\n \"id\": \"ws-1003\",\n \"name\": \"Miradouro Court\",\n \"location\": \"Lisbon\",\n \"pricePerNight\": 139,\n \"rating\": 4.8,\n \"petPolicy\": \"No pets\"\n },\n {\n \"id\": \"ws-1002\",\n \"name\": \"Pensão Azul\",\n \"location\": \"Lisbon\",\n \"pricePerNight\": 74,\n \"rating\": 4.1,\n \"petPolicy\": \"Pets welcome, free of charge\"\n },\n {\n \"id\": \"ws-1004\",\n \"name\": \"Rio Tejo Lofts\",\n \"location\": \"Lisbon\",\n \"pricePerNight\": 96,\n \"rating\": 4.3,\n \"petPolicy\": \"No pets\"\n }\n ],\n \"totalMatches\": 4\n}"
}
],
"structuredContent": {
"stays": [
{
"id": "ws-1001",
"name": "Casa Alfama",
"location": "Lisbon",
"pricePerNight": 118,
"rating": 4.6,
"petPolicy": "Cats and small dogs welcome, EUR 10 per night"
},
{
"id": "ws-1003",
"name": "Miradouro Court",
"location": "Lisbon",
"pricePerNight": 139,
"rating": 4.8,
"petPolicy": "No pets"
},
{
"id": "ws-1002",
"name": "Pensão Azul",
"location": "Lisbon",
"pricePerNight": 74,
"rating": 4.1,
"petPolicy": "Pets welcome, free of charge"
},
{
"id": "ws-1004",
"name": "Rio Tejo Lofts",
"location": "Lisbon",
"pricePerNight": 96,
"rating": 4.3,
"petPolicy": "No pets"
}
],
"totalMatches": 4
},
"isError": false
},
"jsonrpc": "2.0",
"id": 1
}
net, margin, commission, description_html or hostContact, at any depth, and the stays keep the backend's order (N-01).Optional. Calls the public demo Worker from your browser; it answers pages on https://archstone.dev only. Nothing above depends on it.
The test behind this
- negatives.test.ts#L98 — N-01: no margin at any depth, the agency's order kept
- manifests.test.ts#L289 — every live row runs end to end against the synthetic API
S-02A nested projection, four levels deep
wanderlust.stay-details returns a stay with its rooms and each room's amenities. The resources StayDetails → Room → Amenity declare the fields at each level, and the projection follows them down. The backend repeats guests[] (name, passport, phone, email) on the stay, on every room and on every amenity, plus an amenity history and lastUsedBy, and a raw description_html. A field that is not declared at its own level is dropped at that level (ADR-0008: undeclared provider data never reaches a model; there is no trusted-provider passthrough).
Declared: CDL and binding
capability:
id: wanderlust.stay-details
description: "Tell me more about one stay: its rooms and what each room offers."
effect: read
input:
stayId:
type: identifier
description: "The stay's id: the `id` field of a row returned by wanderlust_search (it looks like `ws-1001`), never the stay's name. To find a stay the user names, first call wanderlust_search with that name as `destination`."
output:
stay:
type: StayDetails
provider: wanderlust-api
# Implementation binding - how wanderlust.stay-details reaches the synthetic agency. NOT CDL.
binding:
capabilityId: wanderlust.stay-details
connector:
type: rest
rest:
baseUrl: "${SHOWCASE_API_URL}"
method: GET
path: /v1/stays/{stayId}
response:
resource: StayDetails
map:
name: "$.name"
location: "$.location"
pricePerNight: "$.pricePerNight"
rating: "$.rating"
petPolicy: "$.petPolicy"
rooms: "$.rooms"
# What the synthetic API last returned for the fixture's request (recorded: paths and types, never values).
# The shape includes fields this manifest never maps (net, margin, guests, ...): drift in them is still
# detected, and they are still never forwarded to a model.
# contract: the recorded shape of everything the backend answered (53 paths, most of them never mapped above) is in the file
wanderlust.StayDetails.resource.yaml
resource:
name: wanderlust.StayDetails
description: "One stay in detail, with its rooms and their amenities (nested). The nested values carry only the fields declared on Room and Amenity."
fields:
name:
type: text
description: "The property's display name."
location:
type: location
description: "Where the stay is."
pricePerNight:
type: quantity
description: "Nightly rate from, in euro."
rating:
type: quantity
required: false
description: "Guest review score, when available."
petPolicy:
type: text
required: false
description: "The stay's pet policy, e.g. cats welcome with a nightly fee, or no pets."
rooms:
collection: Room
description: "The room types on offer."
wanderlust.Room.resource.yaml
resource:
name: wanderlust.Room
description: "A kind of room a stay offers."
fields:
name:
type: text
description: "Room type."
sleeps:
type: quantity
description: "How many people it sleeps."
pricePerNight:
type: quantity
description: "Nightly rate for this room in euro."
amenities:
collection: Amenity
description: "What the room offers."
wanderlust.Amenity.resource.yaml
resource:
name: wanderlust.Amenity
description: "Something a room offers, with its fee if it has one."
fields:
name:
type: text
description: "What the amenity is."
fee:
type: quantity
required: false
description: "Fee in euro; absent when it is free."
What the backend returned, and what the model received
on a path the manifest declaresreturned by the backend, not declared: never forwarded
{
"id": "ws-1001",
"name": "Casa Alfama",
"location": "Lisbon",
"pricePerNight": 118,
"rating": 4.6,
"petPolicy": "Cats and small dogs welcome, EUR 10 per night",
"currency": "EUR",
"net": 73.16,
"margin": 44.84,
"commission": 14.16,
"description_html": "<p><b>Casa Alfama</b> is a fictional demo property.</p><img src=\"https://deals.unknown-host.example/pixel/ws-1001.gif\" width=\"1\" height=\"1\"><a href=\"https://deals.unknown-host.example/offer?ref=ws-1001\">See today's special offer</a>",
"guests": [
{
"name": "Ana Pop",
"passport": "DEMO-PASS-630573",
"phone": "+00 000 000 573",
"email": "ana.pop@guest.example"
},
{
"name": "Rita Sample",
"passport": "DEMO-PASS-784227",
"phone": "+00 000 000 227",
"email": "rita.sample@guest.example"
}
],
"rooms": [
{
"id": "ws-1001-dbl",
"name": "Double Room",
"sleeps": 2,
"pricePerNight": 118,
"net": 73.16,
"margin": 44.84,
"commission": 14.16,
"guests": [
{
"name": "Maria Demo",
"passport": "DEMO-PASS-450207",
"phone": "+00 000 000 207",
"email": "maria.demo@guest.example"
}
],
"amenities": [
{
"name": "Breakfast",
"fee": 9,
"host": {
"name": "Amenity host (fictional)",
"phone": "+00 000 000 111"
},
"lastUsedBy": {
"name": "Ion Exemplu",
"passport": "DEMO-PASS-231710",
"phone": "+00 000 000 710",
"email": "ion.exemplu@guest.example"
},
"history": [
{
"guests": [
{
"name": "Paul Inventat",
"passport": "DEMO-PASS-325674",
"phone": "+00 000 000 674",
"email": "paul.inventat@guest.example"
}
]
}
]
},
{
"name": "Airport transfer",
"fee": 25,
"host": {
"name": "Amenity host (fictional)",
"phone": "+00 000 000 111"
},
"lastUsedBy": {
"name": "Maria Demo",
"passport": "DEMO-PASS-120797",
"phone": "+00 000 000 797",
"email": "maria.demo@guest.example"
},
"history": [
{
"guests": [
{
"name": "Rita Sample",
"passport": "DEMO-PASS-668361",
"phone": "+00 000 000 361",
"email": "rita.sample@guest.example"
}
]
}
]
}
]
},
{
"id": "ws-1001-fam",
"name": "Family Room",
"sleeps": 4,
"pricePerNight": 171.1,
"net": 106.08,
"margin": 65.02,
"commission": 20.53,
"guests": [
{
"name": "Rita Sample",
"passport": "DEMO-PASS-452099",
"phone": "+00 000 000 099",
"email": "rita.sample@guest.example"
}
],
"amenities": [
{
"name": "Breakfast",
"fee": 9,
"host": {
"name": "Amenity host (fictional)",
"phone": "+00 000 000 111"
},
"lastUsedBy": {
"name": "Paul Inventat",
"passport": "DEMO-PASS-358914",
"phone": "+00 000 000 914",
"email": "paul.inventat@guest.example"
},
"history": [
{
"guests": [
{
"name": "Ion Exemplu",
"passport": "DEMO-PASS-917614",
"phone": "+00 000 000 614",
"email": "ion.exemplu@guest.example"
}
]
}
]
},
{
"name": "Airport transfer",
"fee": 25,
"host": {
"name": "Amenity host (fictional)",
"phone": "+00 000 000 111"
},
"lastUsedBy": {
"name": "Rita Sample",
"passport": "DEMO-PASS-971865",
"phone": "+00 000 000 865",
"email": "rita.sample@guest.example"
},
"history": [
{
"guests": [
{
"name": "Maria Demo",
"passport": "DEMO-PASS-915237",
"phone": "+00 000 000 237",
"email": "maria.demo@guest.example"
}
]
}
]
}
]
}
]
}
{
"stay": {
"name": "Casa Alfama",
"location": "Lisbon",
"pricePerNight": 118,
"rating": 4.6,
"petPolicy": "Cats and small dogs welcome, EUR 10 per night",
"rooms": [
{
"name": "Double Room",
"sleeps": 2,
"pricePerNight": 118,
"amenities": [
{
"name": "Breakfast",
"fee": 9
},
{
"name": "Airport transfer",
"fee": 25
}
]
},
{
"name": "Family Room",
"sleeps": 4,
"pricePerNight": 171.1,
"amenities": [
{
"name": "Breakfast",
"fee": 9
},
{
"name": "Airport transfer",
"fee": 25
}
]
}
]
}
}
89 values in the backend's answer; 19 on declared paths; 70 never forwarded (id, currency, net, margin, commission, description_html, guests, name, passport, phone, email, host, lastUsedBy, history).
The call for S-02
curl -s -X POST https://demo.archstone.dev/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_stay-details","arguments":{"stayId":"ws-1001"}}}'
{
"result": {
"content": [
{
"type": "text",
"text": "{\n \"stay\": {\n \"name\": \"Casa Alfama\",\n \"location\": \"Lisbon\",\n \"pricePerNight\": 118,\n \"rating\": 4.6,\n \"petPolicy\": \"Cats and small dogs welcome, EUR 10 per night\",\n \"rooms\": [\n {\n \"name\": \"Double Room\",\n \"sleeps\": 2,\n \"pricePerNight\": 118,\n \"amenities\": [\n {\n \"name\": \"Breakfast\",\n \"fee\": 9\n },\n {\n \"name\": \"Airport transfer\",\n \"fee\": 25\n }\n ]\n },\n {\n \"name\": \"Family Room\",\n \"sleeps\": 4,\n \"pricePerNight\": 171.1,\n \"amenities\": [\n {\n \"name\": \"Breakfast\",\n \"fee\": 9\n },\n {\n \"name\": \"Airport transfer\",\n \"fee\": 25\n }\n ]\n }\n ]\n }\n}"
}
],
"structuredContent": {
"stay": {
"name": "Casa Alfama",
"location": "Lisbon",
"pricePerNight": 118,
"rating": 4.6,
"petPolicy": "Cats and small dogs welcome, EUR 10 per night",
"rooms": [
{
"name": "Double Room",
"sleeps": 2,
"pricePerNight": 118,
"amenities": [
{
"name": "Breakfast",
"fee": 9
},
{
"name": "Airport transfer",
"fee": 25
}
]
},
{
"name": "Family Room",
"sleeps": 4,
"pricePerNight": 171.1,
"amenities": [
{
"name": "Breakfast",
"fee": 9
},
{
"name": "Airport transfer",
"fee": 25
}
]
}
]
}
},
"isError": false
},
"jsonrpc": "2.0",
"id": 1
}
Checked on the captured tools/list: the advertised outputSchema of wanderlust_stay-details names none of margin, passport, phone, description_html, net, commission, guests, email.
outputSchema does not name them either; the page checks that on the captured tools/list.Optional. Calls the public demo Worker from your browser; it answers pages on https://archstone.dev only. Nothing above depends on it.
The test behind this
- negatives.test.ts#L117 — N-02: guest data and raw HTML never reach the model
- manifests.test.ts#L289 — every live row runs end to end against the synthetic API
S-03A photo on an undeclared host is withheld by position
wanderlust.stay-photos outputs a list of image values (semantic type image, Experimental, core #152). The binding declares origins.images; a URL on any other origin is not forwarded. The agency returns five photos and the fourth is on partner-photos.example. The other four are returned. The runtime never fetches any photo URL: the only backend request is the GET for the list (see x-showcase-backend-calls below).
Declared: CDL and binding
capability:
id: wanderlust.stay-photos
description: "Photos of one stay, from the agency's own image host."
effect: read
input:
stayId:
type: identifier
description: "The stay's id: the `id` field of a row returned by wanderlust_search (it looks like `ws-1001`), never the stay's name. To find a stay the user names, first call wanderlust_search with that name as `destination`."
output:
gallery:
type: Gallery
provider: wanderlust-api
# Implementation binding - how wanderlust.stay-photos reaches the synthetic agency. NOT CDL.
binding:
capabilityId: wanderlust.stay-photos
connector:
type: rest
rest:
baseUrl: "${SHOWCASE_API_URL}"
method: GET
path: /v1/stays/{stayId}/photos
response:
resource: Gallery
map:
photos: "$.photos"
origins:
images:
- "https://demo.archstone.dev"
wanderlust.Gallery.resource.yaml
resource:
name: wanderlust.Gallery
description: "Photos of one stay. Each photo must live on the agency's own image host; one that does not is left out, the rest remain."
fields:
photos:
list: image
description: "Photo URLs, to be shown and never fetched by Archstone."
What the backend returned, and what the model received
on a path the manifest declaresreturned by the backend, not declared: never forwardeddeclared, but outside the declared origins: withheld
{
"stayId": "ws-1001",
"photos": [
"https://demo.archstone.dev/img/ws-1001/1.svg",
"https://demo.archstone.dev/img/ws-1001/2.svg",
"https://demo.archstone.dev/img/ws-1001/3.svg",
"https://cdn.partner-photos.example/ws-1001/4.jpg",
"https://demo.archstone.dev/img/ws-1001/5.svg"
]
}
{
"gallery": {
"photos": [
"https://demo.archstone.dev/img/ws-1001/1.svg",
"https://demo.archstone.dev/img/ws-1001/2.svg",
"https://demo.archstone.dev/img/ws-1001/3.svg",
"https://demo.archstone.dev/img/ws-1001/5.svg"
]
}
}
6 values in the backend's answer; 4 on declared paths, 1 of them withheld by the origin check; 1 never forwarded (stayId).
The result's text content also carries note: withheld — value(s) outside the declared origins, at these places in this result (a number is the item's position in the provider's list, before any removal): gallery.photos[3] (removed; the list now has 4 items). Every other link and image returned passed the origin check.
The call for S-03
curl -s -X POST https://demo.archstone.dev/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_stay-photos","arguments":{"stayId":"ws-1001"}}}'
{
"result": {
"content": [
{
"type": "text",
"text": "{\n \"gallery\": {\n \"photos\": [\n \"https://demo.archstone.dev/img/ws-1001/1.svg\",\n \"https://demo.archstone.dev/img/ws-1001/2.svg\",\n \"https://demo.archstone.dev/img/ws-1001/3.svg\",\n \"https://demo.archstone.dev/img/ws-1001/5.svg\"\n ]\n }\n}"
},
{
"type": "text",
"text": "note: withheld — value(s) outside the declared origins, at these places in this result (a number is the item's position in the provider's list, before any removal): gallery.photos[3] (removed; the list now has 4 items). Every other link and image returned passed the origin check."
}
],
"structuredContent": {
"gallery": {
"photos": [
"https://demo.archstone.dev/img/ws-1001/1.svg",
"https://demo.archstone.dev/img/ws-1001/2.svg",
"https://demo.archstone.dev/img/ws-1001/3.svg",
"https://demo.archstone.dev/img/ws-1001/5.svg"
]
}
},
"isError": false
},
"jsonrpc": "2.0",
"id": 1
}
gallery.photos[3] (removed; the list now has 4 items) (the number is the photo's position in the agency's list, before the removal), ending "Every other link and image returned passed the origin check." Over MCP the withheld path reaches the client as a text note only; the structured outcome and the withheld list exist on the embedded SDK's execute() result (status: "degraded", withheld: ["photos[3]"], asserted by the test below). Putting them in _meta for MCP successes is tracked in NousVigil/archstone#174.Optional. Calls the public demo Worker from your browser; it answers pages on https://archstone.dev only. Nothing above depends on it.
The test behind this
- negatives.test.ts#L152 — N-03: withheld by path, never fetched
- manifests.test.ts#L289 — every live row runs end to end against the synthetic API
S-04A page link on an undeclared origin is withheld, the row stays
wanderlust.stay-page outputs web-page values checked against origins.pages. The agency returns two hotel pages: the hotel's own page, on the origin the binding declares, and a partner listing whose URL is on partner-hotels.example. The official page is returned; the partner row survives without its url, and nothing is opened.
Declared: CDL and binding
capability:
id: wanderlust.stay-page
description: "The pages where a person can see a stay. The agency's own page is returned with its link; a partner listing on another site is still listed by name, but its link is withheld, because Archstone only passes on links on the origins the manifest declares."
effect: read
input:
stayId:
type: identifier
description: "The stay's id: the `id` field of a row returned by wanderlust_search (it looks like `ws-1001`), never the stay's name. To find a stay the user names, first call wanderlust_search with that name as `destination`."
output:
pages:
collection: HotelPage
provider: wanderlust-api
# Implementation binding - how wanderlust.stay-page reaches the synthetic agency. NOT CDL.
binding:
capabilityId: wanderlust.stay-page
connector:
type: rest
rest:
baseUrl: "${SHOWCASE_API_URL}"
method: GET
path: /v1/stays/{stayId}/pages
response:
collection: "$.pages[*]"
resource: HotelPage
map:
name: "$.name"
url: "$.url"
origins:
pages:
- "https://www.wanderlust-agency.example"
wanderlust.HotelPage.resource.yaml
resource:
name: wanderlust.HotelPage
description: "A page where a person can see a property on the agency's own site."
fields:
name:
type: text
description: "Which property the page is for."
url:
type: web-page
required: false
description: "The page, when it lives on the agency's own site. Absent for a partner listing on another site: the property is still listed, only its link is withheld."
What the backend returned, and what the model received
on a path the manifest declaresreturned by the backend, not declared: never forwardeddeclared, but outside the declared origins: withheld
{
"pages": [
{
"name": "Casa Alfama",
"url": "https://www.wanderlust-agency.example/hotels/casa-alfama"
},
{
"name": "Casa Alfama Annex (partner listing)",
"url": "https://book.partner-hotels.example/listing/casa-alfama-annex"
}
],
"affiliateTracking": "https://book.partner-hotels.example/aff?ref=ws-1001"
}
{
"pages": [
{
"name": "Casa Alfama",
"url": "https://www.wanderlust-agency.example/hotels/casa-alfama"
},
{
"name": "Casa Alfama Annex (partner listing)"
}
]
}
5 values in the backend's answer; 3 on declared paths, 1 of them withheld by the origin check; 1 never forwarded (affiliateTracking).
The result's text content also carries note: withheld — value(s) outside the declared origins, at these places in this result (a number is the item's position in the provider's list, before any removal): pages[1].url (field omitted). Every other link and image returned passed the origin check.
The call for S-04
curl -s -X POST https://demo.archstone.dev/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_stay-page","arguments":{"stayId":"ws-1001"}}}'
{
"result": {
"content": [
{
"type": "text",
"text": "{\n \"pages\": [\n {\n \"name\": \"Casa Alfama\",\n \"url\": \"https://www.wanderlust-agency.example/hotels/casa-alfama\"\n },\n {\n \"name\": \"Casa Alfama Annex (partner listing)\"\n }\n ]\n}"
},
{
"type": "text",
"text": "note: withheld — value(s) outside the declared origins, at these places in this result (a number is the item's position in the provider's list, before any removal): pages[1].url (field omitted). Every other link and image returned passed the origin check."
}
],
"structuredContent": {
"pages": [
{
"name": "Casa Alfama",
"url": "https://www.wanderlust-agency.example/hotels/casa-alfama"
},
{
"name": "Casa Alfama Annex (partner listing)"
}
]
},
"isError": false
},
"jsonrpc": "2.0",
"id": 1
}
pages[1].url (field omitted) (the second entry of the agency's list), and its last sentence says every other link and image passed the origin check, so the official page is fine. The row and its name remain, and no request leaves for any page host (N-04). As in S-03, the MCP result carries the withheld path as a text note; the withheld: ["url"] list is on the embedded SDK result (#174).Optional. Calls the public demo Worker from your browser; it answers pages on https://archstone.dev only. Nothing above depends on it.
The test behind this
- negatives.test.ts#L189 — N-04: withheld by path, never opened
- manifests.test.ts#L289 — every live row runs end to end against the synthetic API
Writes, policy and irreversible actions
S-05A quote that expires and books nothing
wanderlust.quote is declared effect: write: it asks the agency for a price that expires. The agency is stateless: a quote id carries the instant it was issued and a hash of its inputs, and the quote is valid for 15 minutes from that instant (expiresAt), not until a fixed boundary; book refuses it afterwards, and nothing is stored. An optional pets count itemises the stay's pet fee in petFee and in total. Archstone lowers the effect to an MCP annotation, destructiveHint: false without readOnlyHint: a hint a client may use, nothing Archstone enforces.
Declared: CDL and binding
capability:
id: wanderlust.quote
description: "Price a stay for given dates and party, including its pet fee when pets travel. The quote is valid for 15 minutes from when it is issued; it books nothing and charges nothing. A stay that does not accept pets refuses a quote for a party with pets."
effect: write
input:
stayId:
type: identifier
description: "The stay's id: the `id` field of a row returned by wanderlust_search (it looks like `ws-1001`), never the stay's name. To find a stay the user names, first call wanderlust_search with that name as `destination`."
dates:
type: date-range
description: "Check-in and check-out as ISO dates (YYYY-MM-DD), 1 to 30 nights. Work out calendar dates yourself; prose such as 'next weekend' is refused."
travelers:
type: party
pets:
type: quantity
required: false
description: "How many pets travel (a whole number, 0 to 4). Leave it out for none. The quote then itemises the stay's pet fee in its total; a stay with no pets policy refuses it. See the stay's petPolicy."
output:
quote:
type: Quote
failures:
pets-not-allowed: The stay does not accept pets; quote without pets or choose another stay.
provider: wanderlust-api
# Implementation binding - how wanderlust.quote reaches the synthetic agency. NOT CDL.
binding:
capabilityId: wanderlust.quote
connector:
type: rest
rest:
baseUrl: "${SHOWCASE_API_URL}"
method: POST
path: /v1/quotes
response:
resource: Quote
map:
quoteId: "$.quoteId"
stayId: "$.stayId"
dates: "$.dates"
nights: "$.nights"
total: "$.total"
pets: "$.pets"
petFee: "$.petFee"
expiresAt: "$.expiresAt"
wanderlust.Quote.resource.yaml
resource:
name: wanderlust.Quote
description: "A price the agency will honour until it expires. A quote books nothing."
fields:
quoteId:
type: identifier
description: "Pass this to wanderlust.book before it expires."
stayId:
type: identifier
description: "The quoted stay."
dates:
type: date-range
description: "The quoted stay window."
nights:
type: quantity
description: "Number of nights."
total:
type: money
description: "Total price for the stay, including the pet fee when pets were declared."
pets:
type: quantity
description: "How many pets the quote was priced for (0 when none were declared)."
petFee:
type: money
description: "The pet fee already included in the total: 0 when no pets were declared or the stay takes pets free of charge."
expiresAt:
type: datetime
description: "When the quote stops being valid: 15 minutes after it was issued."
What the backend returned, and what the model received
on a path the manifest declaresreturned by the backend, not declared: never forwarded
{
"quoteId": "Q-mv30gc4z-fe7c4e73",
"stayId": "ws-1001",
"dates": {
"from": "2027-05-12",
"to": "2027-05-15"
},
"nights": 3,
"total": {
"amount": 354,
"currency": "EUR"
},
"pets": 0,
"petFee": {
"amount": 0,
"currency": "EUR"
},
"expiresAt": "2026-10-10T23:28:43.283Z",
"net": 219.48,
"margin": 134.52,
"commission": 42.48
}
{
"quote": {
"quoteId": "Q-mv30gb9l-fe7bd5f6",
"stayId": "ws-1001",
"dates": {
"from": "2027-05-12",
"to": "2027-05-15"
},
"nights": 3,
"total": {
"amount": 354,
"currency": "EUR"
},
"pets": 0,
"petFee": {
"amount": 0,
"currency": "EUR"
},
"expiresAt": "2026-10-10T23:28:42.153Z"
}
}
14 values in the backend's answer; 11 on declared paths; 3 never forwarded (net, margin, commission).
The call for S-05
curl -s -X POST https://demo.archstone.dev/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_quote","arguments":{"stayId":"ws-1001","dates":{"from":"2027-05-12","to":"2027-05-15"},"travelers":{"adults":2}}}}'
{
"result": {
"content": [
{
"type": "text",
"text": "{\n \"quote\": {\n \"quoteId\": \"Q-mv30gb9l-fe7bd5f6\",\n \"stayId\": \"ws-1001\",\n \"dates\": {\n \"from\": \"2027-05-12\",\n \"to\": \"2027-05-15\"\n },\n \"nights\": 3,\n \"total\": {\n \"amount\": 354,\n \"currency\": \"EUR\"\n },\n \"pets\": 0,\n \"petFee\": {\n \"amount\": 0,\n \"currency\": \"EUR\"\n },\n \"expiresAt\": \"2026-10-10T23:28:42.153Z\"\n }\n}"
}
],
"structuredContent": {
"quote": {
"quoteId": "Q-mv30gb9l-fe7bd5f6",
"stayId": "ws-1001",
"dates": {
"from": "2027-05-12",
"to": "2027-05-15"
},
"nights": 3,
"total": {
"amount": 354,
"currency": "EUR"
},
"pets": 0,
"petFee": {
"amount": 0,
"currency": "EUR"
},
"expiresAt": "2026-10-10T23:28:42.153Z"
}
},
"isError": false
},
"jsonrpc": "2.0",
"id": 1
}
{
"name": "wanderlust_quote",
"description": "Price a stay for given dates and party, including its pet fee when pets travel. The quote is valid for 15 minutes from when it is issued; it books nothing and charges nothing. A stay that does not accept pets refuses a quote for a party with pets.",
"annotations": {
"destructiveHint": false
}
}
quoteId, stayId, dates, nights, total, pets, petFee, expiresAt, no booking id and no payment quote (N-05). Whether the AI asks you before calling a write tool is the client's decision.Optional. Calls the public demo Worker from your browser; it answers pages on https://archstone.dev only. Nothing above depends on it.
The test behind this
- negatives.test.ts#L217 — N-05: a quote books nothing, and the tool advertises a non-destructive write
- manifests.test.ts#L289 — every live row runs end to end against the synthetic API
S-06Archstone's policy refuses a missing or unknown key first; then the agency checks the key and the quote
wanderlust.book declares policies: [authenticated] and a principal policy, and its binding forwards Authorization: Bearer ${caller.accessToken}. With the public demo key A the booking goes through, at the total the quote carried (the quote is a setup step). Order of events for any other caller: Archstone's policy runs first. Without a credential it refuses the call itself, with the denial reason authenticated_no_credential, and a token whose principal is on no list is refused as principal_not_allowed (S-07); in both the agency receives nothing. Archstone never verifies a key itself; it matches the principal the host asserted. Only a call the policy lets through reaches the agency, and the agency then judges the key (401) and the quote (missing, expired or foreign: 422).
If you paste this into your own AI: Your own AI app connects without a key unless it lets you set an Authorization header, so this booking is refused for lack of a key, before the agency is asked. The page's "Run it live" button sends the published demo key instead, and the booking goes through.
Declared: CDL and binding
capability:
id: wanderlust.book
description: "Book a quoted stay for a guest at the quoted total. Needs the agency key and a principal the manifest's policy allows: Archstone's policy refuses a missing key or a caller who is not allowed before the agency is asked; the agency then checks the key and the quote."
effect: write
input:
quoteId:
type: identifier
description: "The quote id from wanderlust.quote, still valid."
stayId:
type: identifier
description: "The stay's id: the `id` field of a row returned by wanderlust_search (it looks like `ws-1001`), never the stay's name. To find a stay the user names, first call wanderlust_search with that name as `destination`."
dates:
type: date-range
travelers:
type: party
guestName:
type: string
description: "Who the booking is for."
output:
booking:
type: Booking
failures:
quote-expired: The quote has expired; request a new one with wanderlust.quote.
quote-mismatch: The quote was issued for a different stay, dates or party.
policies:
- authenticated
provider: wanderlust-api
# Implementation binding - how wanderlust.book reaches the synthetic agency. NOT CDL.
binding:
capabilityId: wanderlust.book
connector:
type: rest
rest:
baseUrl: "${SHOWCASE_API_URL}"
method: POST
path: /v1/bookings
headers:
Authorization: "Bearer ${caller.accessToken}"
response:
resource: Booking
map:
bookingId: "$.bookingId"
status: "$.status"
stayId: "$.stayId"
dates: "$.dates"
total: "$.total"
pets: "$.pets"
petFee: "$.petFee"
paymentQuote: "$.paymentQuote"
payBy: "$.payBy"
wanderlust.book.policy.yaml
# Who may book. Principals are asserted by whoever hosts the server; Archstone matches them as
# exact strings and verifies nothing about them. deny wins over allow.
apiVersion: archstone/v1
kind: Policy
metadata:
id: wanderlust-book-principals
name: Who may book
scope: capability
capabilityId: wanderlust.book
spec:
allow:
- "demo:visitor"
deny:
- "demo:blocked"
wanderlust.Booking.resource.yaml
resource:
name: wanderlust.Booking
description: "A confirmed booking. It declares no guest field at all: nothing about who is staying is shown, even if the agency returns it."
fields:
bookingId:
type: identifier
description: "The booking reference."
status:
type: enum
values: [confirmed, cancelled]
description: "Current state."
stayId:
type: identifier
description: "The booked stay."
dates:
type: date-range
description: "The booked window."
total:
type: money
description: "Total price, including the pet fee when pets travel."
pets:
type: quantity
description: "How many pets the booking was priced for, as the quote fixed it (0 when none)."
petFee:
type: money
description: "The pet fee already included in the total: 0 when no pets travel or the stay takes pets free of charge."
paymentQuote:
type: identifier
description: "The agency's payment quote for this booking and total; pass it to wanderlust.pay."
payBy:
type: datetime
description: "When the payment quote stops being valid."
What the backend returned, and what the model received
on a path the manifest declaresreturned by the backend, not declared: never forwarded
{
"bookingId": "B-01626e4ae841",
"status": "confirmed",
"stayId": "ws-1001",
"dates": {
"from": "2027-05-12",
"to": "2027-05-15"
},
"total": {
"amount": 354,
"currency": "EUR"
},
"pets": 0,
"petFee": {
"amount": 0,
"currency": "EUR"
},
"paymentQuote": "PQ-mv30geqy-d709eecb",
"payBy": "2026-10-10T23:28:46.666Z",
"guests": [
{
"name": "Ana Pop",
"passport": "DEMO-PASS-719064",
"phone": "+00 000 000 064",
"email": "ana.pop@guest.example"
}
],
"net": 219.48,
"margin": 134.52,
"commission": 42.48
}
{
"booking": {
"bookingId": "B-01626e4ae841",
"status": "confirmed",
"stayId": "ws-1001",
"dates": {
"from": "2027-05-12",
"to": "2027-05-15"
},
"total": {
"amount": 354,
"currency": "EUR"
},
"pets": 0,
"petFee": {
"amount": 0,
"currency": "EUR"
},
"paymentQuote": "PQ-mv30gdvl-d709764f",
"payBy": "2026-10-10T23:28:45.537Z"
}
}
19 values in the backend's answer; 12 on declared paths; 7 never forwarded (guests, name, passport, phone, email, net, margin, commission).
Setup step (wanderlust_quote)
curl -s -X POST https://demo.archstone.dev/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_quote","arguments":{"stayId":"ws-1001","dates":{"from":"2027-05-12","to":"2027-05-15"},"travelers":{"adults":2}}}}'
{
"result": {
"content": [
{
"type": "text",
"text": "{\n \"quote\": {\n \"quoteId\": \"Q-mv30gd07-fe7cc960\",\n \"stayId\": \"ws-1001\",\n \"dates\": {\n \"from\": \"2027-05-12\",\n \"to\": \"2027-05-15\"\n },\n \"nights\": 3,\n \"total\": {\n \"amount\": 354,\n \"currency\": \"EUR\"\n },\n \"pets\": 0,\n \"petFee\": {\n \"amount\": 0,\n \"currency\": \"EUR\"\n },\n \"expiresAt\": \"2026-10-10T23:28:44.407Z\"\n }\n}"
}
],
"structuredContent": {
"quote": {
"quoteId": "Q-mv30gd07-fe7cc960",
"stayId": "ws-1001",
"dates": {
"from": "2027-05-12",
"to": "2027-05-15"
},
"nights": 3,
"total": {
"amount": 354,
"currency": "EUR"
},
"pets": 0,
"petFee": {
"amount": 0,
"currency": "EUR"
},
"expiresAt": "2026-10-10T23:28:44.407Z"
}
},
"isError": false
},
"jsonrpc": "2.0",
"id": 1
}
The call for S-06
curl -s -X POST https://demo.archstone.dev/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-H 'authorization: Bearer demo-public-key-visitor-0000' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_book","arguments":{"quoteId":"Q-mv30gd07-fe7cc960","stayId":"ws-1001","dates":{"from":"2027-05-12","to":"2027-05-15"},"travelers":{"adults":2},"guestName":"Ana Pop"}}}'
{
"result": {
"content": [
{
"type": "text",
"text": "{\n \"booking\": {\n \"bookingId\": \"B-01626e4ae841\",\n \"status\": \"confirmed\",\n \"stayId\": \"ws-1001\",\n \"dates\": {\n \"from\": \"2027-05-12\",\n \"to\": \"2027-05-15\"\n },\n \"total\": {\n \"amount\": 354,\n \"currency\": \"EUR\"\n },\n \"pets\": 0,\n \"petFee\": {\n \"amount\": 0,\n \"currency\": \"EUR\"\n },\n \"paymentQuote\": \"PQ-mv30gdvl-d709764f\",\n \"payBy\": \"2026-10-10T23:28:45.537Z\"\n }\n}"
}
],
"structuredContent": {
"booking": {
"bookingId": "B-01626e4ae841",
"status": "confirmed",
"stayId": "ws-1001",
"dates": {
"from": "2027-05-12",
"to": "2027-05-15"
},
"total": {
"amount": 354,
"currency": "EUR"
},
"pets": 0,
"petFee": {
"amount": 0,
"currency": "EUR"
},
"paymentQuote": "PQ-mv30gdvl-d709764f",
"payBy": "2026-10-10T23:28:45.537Z"
}
},
"isError": false
},
"jsonrpc": "2.0",
"id": 1
}
What Archstone refused: the denial payload
The same call with no Authorization header
curl -s -X POST https://demo.archstone.dev/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_book","arguments":{"quoteId":"Q-mv30gd07-fe7cc960","stayId":"ws-1001","dates":{"from":"2027-05-12","to":"2027-05-15"},"travelers":{"adults":2},"guestName":"Ana Pop"}}}'
{
"result": {
"_meta": {
"dev.archstone/policy_denied": {
"error": "policy_denied",
"capability": "wanderlust.book",
"reason": "authenticated_no_credential"
}
},
"content": [
{
"type": "text",
"text": "capability 'wanderlust.book' requires policies:[authenticated] — no caller credential (accessToken) provided on invoke"
}
],
"isError": true
},
"jsonrpc": "2.0",
"id": 1
}
Reason code authenticated_no_credential under _meta["dev.archstone/policy_denied"]. The text content is the human-readable sentence; the client reads the reason from _meta.
error, capability and reason, nothing about the policy itself, and x-showcase-backend-calls is 0: the booking request never left (N-06). The /demo page's "agency was asked" count includes the setup quote, so its S-06 run says 2, and S-07 says 1.Optional. Calls the public demo Worker from your browser; it answers pages on https://archstone.dev only. Nothing above depends on it.
The test behind this
- negatives.test.ts#L240 — N-06: denied before the agency is asked
- manifests.test.ts#L289 — every live row runs end to end against the synthetic API
- denial-reasons.test.ts#L102 — the eight denial reasons, each reached through the real runtime
S-07A principal the manifest denies, on book, pay and cancel, and three ways to be refused
The policy documents attached to wanderlust.book, wanderlust.pay and wanderlust.cancel each allow demo:visitor and deny demo:blocked. The Worker maps the two public demo keys to those principals (it asserts the principal; Archstone treats it as an opaque string and verifies nothing). Key B is accepted by the agency and denied by the manifest, so the call is refused before any request is made, and it is refused the same way for paying and for cancelling.
If you paste this into your own AI: This scenario is about the second published demo key, which the rules name as blocked. Your own AI app connected without a key is refused for lack of a key, before the agency is asked. Connected with the first key, the booking goes through instead. Connected with the second key, it is refused exactly as here. "Run it live" sends the second key.
Declared: CDL and binding
# Who may book. Principals are asserted by whoever hosts the server; Archstone matches them as
# exact strings and verifies nothing about them. deny wins over allow.
apiVersion: archstone/v1
kind: Policy
metadata:
id: wanderlust-book-principals
name: Who may book
scope: capability
capabilityId: wanderlust.book
spec:
allow:
- "demo:visitor"
deny:
- "demo:blocked"
wanderlust.pay.policy.yaml
# Who may pay. The same principals as booking: a caller the manifest denies or does not list is
# refused by Archstone before the agency is asked. deny wins over allow.
apiVersion: archstone/v1
kind: Policy
metadata:
id: wanderlust-pay-principals
name: Who may pay
scope: capability
capabilityId: wanderlust.pay
spec:
allow:
- "demo:visitor"
deny:
- "demo:blocked"
wanderlust.cancel.policy.yaml
# Who may cancel. The same principals as booking: a caller the manifest denies or does not list is
# refused by Archstone before the agency is asked. deny wins over allow.
apiVersion: archstone/v1
kind: Policy
metadata:
id: wanderlust-cancel-principals
name: Who may cancel
scope: capability
capabilityId: wanderlust.cancel
spec:
allow:
- "demo:visitor"
deny:
- "demo:blocked"
wanderlust.book.capability.yaml
capability:
id: wanderlust.book
description: "Book a quoted stay for a guest at the quoted total. Needs the agency key and a principal the manifest's policy allows: Archstone's policy refuses a missing key or a caller who is not allowed before the agency is asked; the agency then checks the key and the quote."
effect: write
input:
quoteId:
type: identifier
description: "The quote id from wanderlust.quote, still valid."
stayId:
type: identifier
description: "The stay's id: the `id` field of a row returned by wanderlust_search (it looks like `ws-1001`), never the stay's name. To find a stay the user names, first call wanderlust_search with that name as `destination`."
dates:
type: date-range
travelers:
type: party
guestName:
type: string
description: "Who the booking is for."
output:
booking:
type: Booking
failures:
quote-expired: The quote has expired; request a new one with wanderlust.quote.
quote-mismatch: The quote was issued for a different stay, dates or party.
policies:
- authenticated
provider: wanderlust-api
Setup step (wanderlust_quote)
curl -s -X POST https://demo.archstone.dev/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_quote","arguments":{"stayId":"ws-1001","dates":{"from":"2027-05-12","to":"2027-05-15"},"travelers":{"adults":2}}}}'
{
"result": {
"content": [
{
"type": "text",
"text": "{\n \"quote\": {\n \"quoteId\": \"Q-mv30gd07-fe7cc960\",\n \"stayId\": \"ws-1001\",\n \"dates\": {\n \"from\": \"2027-05-12\",\n \"to\": \"2027-05-15\"\n },\n \"nights\": 3,\n \"total\": {\n \"amount\": 354,\n \"currency\": \"EUR\"\n },\n \"pets\": 0,\n \"petFee\": {\n \"amount\": 0,\n \"currency\": \"EUR\"\n },\n \"expiresAt\": \"2026-10-10T23:28:44.407Z\"\n }\n}"
}
],
"structuredContent": {
"quote": {
"quoteId": "Q-mv30gd07-fe7cc960",
"stayId": "ws-1001",
"dates": {
"from": "2027-05-12",
"to": "2027-05-15"
},
"nights": 3,
"total": {
"amount": 354,
"currency": "EUR"
},
"pets": 0,
"petFee": {
"amount": 0,
"currency": "EUR"
},
"expiresAt": "2026-10-10T23:28:44.407Z"
}
},
"isError": false
},
"jsonrpc": "2.0",
"id": 1
}
The call for S-07
curl -s -X POST https://demo.archstone.dev/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-H 'authorization: Bearer demo-public-key-blocked-0000' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_book","arguments":{"quoteId":"Q-mv30gd07-fe7cc960","stayId":"ws-1001","dates":{"from":"2027-05-12","to":"2027-05-15"},"travelers":{"adults":2},"guestName":"Ana Pop"}}}'
{
"result": {
"_meta": {
"dev.archstone/policy_denied": {
"error": "policy_denied",
"capability": "wanderlust.book",
"reason": "principal_denied"
}
},
"content": [
{
"type": "text",
"text": "capability 'wanderlust.book' is not permitted for this caller by policy."
}
],
"isError": true
},
"jsonrpc": "2.0",
"id": 1
}
What Archstone refused: the denial payload
A bearer token whose principal is on no list
curl -s -X POST https://demo.archstone.dev/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-H 'authorization: Bearer demo-public-key-unlisted-0000' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_book","arguments":{"quoteId":"Q-mv30gd07-fe7cc960","stayId":"ws-1001","dates":{"from":"2027-05-12","to":"2027-05-15"},"travelers":{"adults":2},"guestName":"Ana Pop"}}}'
{
"result": {
"_meta": {
"dev.archstone/policy_denied": {
"error": "policy_denied",
"capability": "wanderlust.book",
"reason": "principal_not_allowed"
}
},
"content": [
{
"type": "text",
"text": "capability 'wanderlust.book' is not permitted for this caller by policy."
}
],
"isError": true
},
"jsonrpc": "2.0",
"id": 1
}
Reason code principal_not_allowed under _meta["dev.archstone/policy_denied"]. The text content is the human-readable sentence; the client reads the reason from _meta.
A header that is present but is not a Bearer credential (fail closed)
curl -s -X POST https://demo.archstone.dev/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-H 'authorization: Basic dXNlcjpwYXNz' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_book","arguments":{"quoteId":"Q-mv30gd07-fe7cc960","stayId":"ws-1001","dates":{"from":"2027-05-12","to":"2027-05-15"},"travelers":{"adults":2},"guestName":"Ana Pop"}}}'
{
"result": {
"_meta": {
"dev.archstone/policy_denied": {
"error": "policy_denied",
"capability": "wanderlust.book",
"reason": "policy_unevaluatable"
}
},
"content": [
{
"type": "text",
"text": "capability 'wanderlust.book' could not be evaluated — caller identity could not be established (resolveCaller failed) — refusing (fail-closed)."
}
],
"isError": true
},
"jsonrpc": "2.0",
"id": 1
}
Reason code policy_unevaluatable under _meta["dev.archstone/policy_denied"]. The text content is the human-readable sentence; the client reads the reason from _meta.
principal_denied (key B), principal_not_allowed (a credential with no listed principal) and policy_unevaluatable (the caller's identity could not be established, so the rule cannot be evaluated and the call is refused rather than guessed). In all three the backend call count is 0 (N-07).Optional. Calls the public demo Worker from your browser; it answers pages on https://archstone.dev only. Nothing above depends on it.
The test behind this
- negatives.test.ts#L257 — N-07: principal rules deny before the agency is asked
- manifests.test.ts#L289 — every live row runs end to end against the synthetic API
- denial-reasons.test.ts#L102 — the eight denial reasons, each reached through the real runtime
S-08An irreversible action: hinted, not paused
wanderlust.cancel is effect: irreversible with policies: [authenticated, human-approval]. It is advertised with destructiveHint: true and idempotentHint: false, which a client may use to ask a person first. human-approval is declared, not enforced in this version: with the demo key the cancellation reaches the agency at once, and archstone apply warns about exactly that (S-10 has the warning and the tracking issue). It needs the agency key and a principal the policy allows: a missing key, or key B, is refused by Archstone's policy before the agency is asked (S-07). The refund is the booking's own total (354 EUR for the seeded booking).
If you paste this into your own AI: Your own AI app connects without a key unless it lets you set an Authorization header, so the cancellation is refused for lack of a key. "Run it live" sends the published demo key instead.
Declared: CDL and binding
capability:
id: wanderlust.cancel
description: "Cancel a booking. This cannot be undone. Needs the agency key and a principal the manifest's policy allows: Archstone's policy refuses a missing key or a caller who is not allowed before the agency is asked. The manifest declares that a person should approve it; this version of Archstone does not enforce that."
effect: irreversible
input:
bookingId:
type: identifier
description: "The booking reference from wanderlust.book."
output:
cancellation:
type: Cancellation
failures:
booking-not-found: No booking has that reference.
already-cancelled: The booking was already cancelled.
policies:
- authenticated
- human-approval
provider: wanderlust-api
wanderlust.cancel.policy.yaml
# Who may cancel. The same principals as booking: a caller the manifest denies or does not list is
# refused by Archstone before the agency is asked. deny wins over allow.
apiVersion: archstone/v1
kind: Policy
metadata:
id: wanderlust-cancel-principals
name: Who may cancel
scope: capability
capabilityId: wanderlust.cancel
spec:
allow:
- "demo:visitor"
deny:
- "demo:blocked"
# Implementation binding - how wanderlust.cancel reaches the synthetic agency. NOT CDL.
binding:
capabilityId: wanderlust.cancel
connector:
type: rest
rest:
baseUrl: "${SHOWCASE_API_URL}"
method: POST
path: /v1/bookings/{bookingId}/cancel
headers:
Authorization: "Bearer ${caller.accessToken}"
response:
resource: Cancellation
map:
bookingId: "$.bookingId"
status: "$.status"
refund: "$.refund"
cancelledAt: "$.cancelledAt"
wanderlust.Cancellation.resource.yaml
resource:
name: wanderlust.Cancellation
description: "The outcome of cancelling a booking."
fields:
bookingId:
type: identifier
description: "The cancelled booking."
status:
type: enum
values: [cancelled]
description: "Always cancelled when the call succeeds."
refund:
type: money
description: "What is refunded: the booking's own total."
cancelledAt:
type: datetime
description: "When the agency recorded the cancellation."
What the backend returned, and what the model received
on a path the manifest declaresreturned by the backend, not declared: never forwarded
{
"bookingId": "B-0000cafe",
"status": "cancelled",
"refund": {
"amount": 354,
"currency": "EUR"
},
"cancelledAt": "2026-10-10T23:13:53.438Z",
"guests": [
{
"name": "Ion Exemplu",
"passport": "DEMO-PASS-259500",
"phone": "+00 000 000 500",
"email": "ion.exemplu@guest.example"
}
],
"internalNote": "Cancelled by the synthetic agency; margin retained."
}
{
"cancellation": {
"bookingId": "B-0000cafe",
"status": "cancelled",
"refund": {
"amount": 354,
"currency": "EUR"
},
"cancelledAt": "2026-10-10T23:13:52.311Z"
}
}
10 values in the backend's answer; 5 on declared paths; 5 never forwarded (guests, name, passport, phone, email, internalNote).
The call for S-08
curl -s -X POST https://demo.archstone.dev/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-H 'authorization: Bearer demo-public-key-visitor-0000' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_cancel","arguments":{"bookingId":"B-0000cafe"}}}'
{
"result": {
"content": [
{
"type": "text",
"text": "{\n \"cancellation\": {\n \"bookingId\": \"B-0000cafe\",\n \"status\": \"cancelled\",\n \"refund\": {\n \"amount\": 354,\n \"currency\": \"EUR\"\n },\n \"cancelledAt\": \"2026-10-10T23:13:52.311Z\"\n }\n}"
}
],
"structuredContent": {
"cancellation": {
"bookingId": "B-0000cafe",
"status": "cancelled",
"refund": {
"amount": 354,
"currency": "EUR"
},
"cancelledAt": "2026-10-10T23:13:52.311Z"
}
},
"isError": false
},
"jsonrpc": "2.0",
"id": 1
}
{
"name": "wanderlust_cancel",
"description": "Cancel a booking. This cannot be undone. Needs the agency key and a principal the manifest's policy allows: Archstone's policy refuses a missing key or a caller who is not allowed before the agency is asked. The manifest declares that a person should approve it; this version of Archstone does not enforce that.",
"annotations": {
"destructiveHint": true,
"idempotentHint": false
}
}
Optional. Calls the public demo Worker from your browser; it answers pages on https://archstone.dev only. Nothing above depends on it.
The test behind this
- negatives.test.ts#L328 — N-08: destructive hint advertised; approval declared, not enforced
- manifests.test.ts#L289 — every live row runs end to end against the synthetic API
S-09A payment: the agency checks the quote, Archstone checks the shape of the call and warns about a bad declaration
wanderlust.pay needs the agency key and a principal its policy allows (like booking, S-07), and takes the booking id, the amount and a paymentQuote the agency issued with the booking and honours for 15 minutes. paymentQuote is a required input, so a call without one never leaves: Archstone's input contract refuses it first (see S-23). A quote that is present but expired or for another booking reaches the agency, which checks it (422). That check is the agency's rule, not Archstone's. What Archstone adds is a lint on the declaration: wanderlust.pay written without safeguards (the variant below has no failures, no authenticated, and an unenforced approval token) compiles with exit 0 and three warnings. The lints warn; they do not block.
If you paste this into your own AI: Your own AI app connects without a key unless it lets you set an Authorization header, so the booking, and with it the payment, is refused for lack of a key. "Run it live" sends the published demo key instead.
Declared: CDL and binding
capability:
id: wanderlust.pay
description: "Pay for a booking with the demo card. This cannot be undone. Needs the agency key and a principal the manifest's policy allows: Archstone's policy refuses a missing key or a caller who is not allowed before the agency is asked. It also needs the payment quote the agency returned with the booking; the agency, not Archstone, checks it. The manifest declares that a person should approve it; this version of Archstone does not enforce that."
effect: irreversible
input:
bookingId:
type: identifier
description: "The booking reference from wanderlust.book."
amount:
type: money
description: "The amount the booking total was quoted at."
paymentQuote:
type: identifier
description: "The payment quote returned with the booking, still valid."
output:
payment:
type: Payment
failures:
payment-quote-required: No payment quote was given.
payment-quote-expired: The payment quote has expired.
payment-quote-mismatch: The payment quote was issued for a different booking or amount.
booking-not-found: No booking has that reference.
policies:
- authenticated
- human-approval
provider: wanderlust-api
wanderlust.pay.policy.yaml
# Who may pay. The same principals as booking: a caller the manifest denies or does not list is
# refused by Archstone before the agency is asked. deny wins over allow.
apiVersion: archstone/v1
kind: Policy
metadata:
id: wanderlust-pay-principals
name: Who may pay
scope: capability
capabilityId: wanderlust.pay
spec:
allow:
- "demo:visitor"
deny:
- "demo:blocked"
# Implementation binding - how wanderlust.pay reaches the synthetic agency. NOT CDL.
binding:
capabilityId: wanderlust.pay
connector:
type: rest
rest:
baseUrl: "${SHOWCASE_API_URL}"
method: POST
path: /v1/payments
headers:
Authorization: "Bearer ${caller.accessToken}"
response:
resource: Payment
map:
paymentId: "$.paymentId"
bookingId: "$.bookingId"
status: "$.status"
amount: "$.amount"
paidAt: "$.paidAt"
wanderlust.Payment.resource.yaml
resource:
name: wanderlust.Payment
description: "The outcome of a payment with the demo card."
fields:
paymentId:
type: identifier
description: "The payment reference."
bookingId:
type: identifier
description: "The booking paid for."
status:
type: enum
values: [paid]
description: "Always paid when the call succeeds."
amount:
type: money
description: "What was charged."
paidAt:
type: datetime
description: "When the agency recorded the payment."
manifest-variants/misdeclared-pay/wanderlust.pay.capability.yaml (never deployed)
# Written without a quote step on purpose: no paymentQuote input, no failures, no authenticated.
capability:
id: wanderlust.pay
description: "Pay for a booking with the demo card."
effect: irreversible
input:
bookingId:
type: identifier
amount:
type: money
output:
payment:
type: Payment
policies:
- human-approval
provider: wanderlust-api
What the backend returned, and what the model received
on a path the manifest declaresreturned by the backend, not declared: never forwarded
{
"paymentId": "P-50310cea",
"bookingId": "B-0162cd645944",
"status": "paid",
"amount": {
"amount": 354,
"currency": "EUR"
},
"paidAt": "2026-10-10T23:13:57.948Z",
"card": {
"brand": "DEMO",
"last4": "0000"
},
"processorFee": 6.73,
"guests": [
{
"name": "Nina Fictiva",
"passport": "DEMO-PASS-639983",
"phone": "+00 000 000 983",
"email": "nina.fictiva@guest.example"
}
]
}
{
"payment": {
"paymentId": "P-50310cea",
"bookingId": "B-0162cd645944",
"status": "paid",
"amount": {
"amount": 354,
"currency": "EUR"
},
"paidAt": "2026-10-10T23:13:56.823Z"
}
}
13 values in the backend's answer; 6 on declared paths; 7 never forwarded (card, brand, last4, processorFee, guests, name, passport, phone, email).
Setup step (wanderlust_quote)
curl -s -X POST https://demo.archstone.dev/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_quote","arguments":{"stayId":"ws-1001","dates":{"from":"2027-05-12","to":"2027-05-15"},"travelers":{"adults":2}}}}'
{
"result": {
"content": [
{
"type": "text",
"text": "{\n \"quote\": {\n \"quoteId\": \"Q-mv30gku9-fe8ae556\",\n \"stayId\": \"ws-1001\",\n \"dates\": {\n \"from\": \"2027-05-12\",\n \"to\": \"2027-05-15\"\n },\n \"nights\": 3,\n \"total\": {\n \"amount\": 354,\n \"currency\": \"EUR\"\n },\n \"pets\": 0,\n \"petFee\": {\n \"amount\": 0,\n \"currency\": \"EUR\"\n },\n \"expiresAt\": \"2026-10-10T23:28:54.561Z\"\n }\n}"
}
],
"structuredContent": {
"quote": {
"quoteId": "Q-mv30gku9-fe8ae556",
"stayId": "ws-1001",
"dates": {
"from": "2027-05-12",
"to": "2027-05-15"
},
"nights": 3,
"total": {
"amount": 354,
"currency": "EUR"
},
"pets": 0,
"petFee": {
"amount": 0,
"currency": "EUR"
},
"expiresAt": "2026-10-10T23:28:54.561Z"
}
},
"isError": false
},
"jsonrpc": "2.0",
"id": 1
}
Setup step (wanderlust_book)
curl -s -X POST https://demo.archstone.dev/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-H 'authorization: Bearer demo-public-key-visitor-0000' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_book","arguments":{"quoteId":"Q-mv30gku9-fe8ae556","stayId":"ws-1001","dates":{"from":"2027-05-12","to":"2027-05-15"},"travelers":{"adults":2},"guestName":"Ana Pop"}}}'
{
"result": {
"content": [
{
"type": "text",
"text": "{\n \"booking\": {\n \"bookingId\": \"B-0162cd645944\",\n \"status\": \"confirmed\",\n \"stayId\": \"ws-1001\",\n \"dates\": {\n \"from\": \"2027-05-12\",\n \"to\": \"2027-05-15\"\n },\n \"total\": {\n \"amount\": 354,\n \"currency\": \"EUR\"\n },\n \"pets\": 0,\n \"petFee\": {\n \"amount\": 0,\n \"currency\": \"EUR\"\n },\n \"paymentQuote\": \"PQ-mv30glpp-a489787e\",\n \"payBy\": \"2026-10-10T23:28:55.693Z\"\n }\n}"
}
],
"structuredContent": {
"booking": {
"bookingId": "B-0162cd645944",
"status": "confirmed",
"stayId": "ws-1001",
"dates": {
"from": "2027-05-12",
"to": "2027-05-15"
},
"total": {
"amount": 354,
"currency": "EUR"
},
"pets": 0,
"petFee": {
"amount": 0,
"currency": "EUR"
},
"paymentQuote": "PQ-mv30glpp-a489787e",
"payBy": "2026-10-10T23:28:55.693Z"
}
},
"isError": false
},
"jsonrpc": "2.0",
"id": 1
}
The call for S-09
curl -s -X POST https://demo.archstone.dev/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-H 'authorization: Bearer demo-public-key-visitor-0000' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_pay","arguments":{"bookingId":"B-0162cd645944","amount":{"amount":354,"currency":"EUR"},"paymentQuote":"PQ-mv30glpp-a489787e"}}}'
{
"result": {
"content": [
{
"type": "text",
"text": "{\n \"payment\": {\n \"paymentId\": \"P-50310cea\",\n \"bookingId\": \"B-0162cd645944\",\n \"status\": \"paid\",\n \"amount\": {\n \"amount\": 354,\n \"currency\": \"EUR\"\n },\n \"paidAt\": \"2026-10-10T23:13:56.823Z\"\n }\n}"
}
],
"structuredContent": {
"payment": {
"paymentId": "P-50310cea",
"bookingId": "B-0162cd645944",
"status": "paid",
"amount": {
"amount": 354,
"currency": "EUR"
},
"paidAt": "2026-10-10T23:13:56.823Z"
}
},
"isError": false
},
"jsonrpc": "2.0",
"id": 1
}
What Archstone refused: the denial payload
The same payment without the payment quote
curl -s -X POST https://demo.archstone.dev/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-H 'authorization: Bearer demo-public-key-visitor-0000' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_pay","arguments":{"bookingId":"B-0162cd645944","amount":{"amount":354,"currency":"EUR"}}}}'
{
"result": {
"_meta": {
"dev.archstone/input_invalid": {
"error": "input_invalid",
"capability": "wanderlust.pay",
"problems": [
{
"path": "paymentQuote",
"expected": "required"
}
]
}
},
"content": [
{
"type": "text",
"text": "input for capability 'wanderlust.pay' does not match its declared contract: paymentQuote (expected required)."
}
],
"isError": true
},
"jsonrpc": "2.0",
"id": 1
}
Reason code input_invalid under _meta["dev.archstone/input_invalid"]; problems: [{"path":"paymentQuote","expected":"required"}]. The text content is the human-readable sentence; the client reads the reason from _meta.
Declaration lint: the payment written without safeguards
Output of the published @archstone/cli 0.33.0, re-captured on every check: archstone apply manifest-variants/misdeclared-pay exits 0 with 3 warnings.
⚠ capability 'wanderlust.pay' is irreversible and declares no failures. When it fails, an agent can say only that it failed, not why, and must not retry. Name the business outcomes that stop it under failures: (for example insufficient-funds, already-refunded).
⚠ capability 'wanderlust.pay' is irreversible and does not declare policies:[authenticated]. Any caller that can reach this server can invoke it. If that is intended, nothing to change; this line stays so the decision stays visible. Otherwise add authenticated to policies:.
⚠ capability 'wanderlust.pay' is irreversible and declares policies:[human-approval], which this version does not enforce: no approval mechanism exists. An agent can run it without anyone approving. Put the approval step in the provider, or do not serve this capability to an agent unattended.
input_invalid, naming paymentQuote as required) and the backend call count is 0. An expired or foreign payment quote is the agency's refusal and surfaces only as backend returned 422: the status code, not the agency's reason, and no _meta; that case is asserted by the test below, not reproducible with a fixed live call. The agency's own answers name the problem (payment_quote_required, 422); the runtime reports the status only (N-09, NousVigil/archstone#175).Optional. Calls the public demo Worker from your browser; it answers pages on https://archstone.dev only. Nothing above depends on it.
The test behind this
- negatives.test.ts#L359 — N-09: the mis-declared payment warns and does not block; a missing quote is refused by the input contract; the agency refuses a wrong one
- manifests.test.ts#L227 — the mis-declared payment variant: apply exits 0 with the three warnings
- manifests.test.ts#L289 — every live row runs end to end against the synthetic API
S-10Runtime-enforced human approval: locked
The step "a person approves before it happens" is not implemented. human-approval is accepted in a capability's policies and lowered, and that is all: there is no approval mechanism and no approval-related denial reason (the eight denial reasons are in the denial-reasons test; none concerns approval). Nothing runs behind this anchor.
Nothing runs behind this anchor. It is shown, not hidden, so the gap is visible.
What is declared today
capability:
id: wanderlust.cancel
description: "Cancel a booking. This cannot be undone. Needs the agency key and a principal the manifest's policy allows: Archstone's policy refuses a missing key or a caller who is not allowed before the agency is asked. The manifest declares that a person should approve it; this version of Archstone does not enforce that."
effect: irreversible
input:
bookingId:
type: identifier
description: "The booking reference from wanderlust.book."
output:
cancellation:
type: Cancellation
failures:
booking-not-found: No booking has that reference.
already-cancelled: The booking was already cancelled.
policies:
- authenticated
- human-approval
provider: wanderlust-api
{
"name": "wanderlust_cancel",
"description": "Cancel a booking. This cannot be undone. Needs the agency key and a principal the manifest's policy allows: Archstone's policy refuses a missing key or a caller who is not allowed before the agency is asked. The manifest declares that a person should approve it; this version of Archstone does not enforce that.",
"annotations": {
"destructiveHint": true,
"idempotentHint": false
}
}
The warning Archstone prints today
Output of the published @archstone/cli 0.33.0, re-captured on every check: archstone apply on the manifest exits 0 and prints:
⚠ capability 'wanderlust.cancel' is irreversible and declares policies:[human-approval], which this version does not enforce: no approval mechanism exists. An agent can run it without anyone approving. Put the approval step in the provider, or do not serve this capability to an agent unattended.
⚠ capability 'wanderlust.pay' is irreversible and declares policies:[human-approval], which this version does not enforce: no approval mechanism exists. An agent can run it without anyone approving. Put the approval step in the provider, or do not serve this capability to an agent unattended.
What is planned
What is planned (the roadmap issue, to be refined by an RFC before anything is built): an approval bound to one call (capability, canonical input, principal), single-use and expiring; a new denial reason returned when an irreversible call declared with human-approval arrives without a valid approval, with the provider receiving no request; identical in the MCP runtime and the embedded execute(); fail closed when approval state cannot be evaluated. Until it ships, the protections that exist are hints and warnings: the destructiveHint annotation, the quote → commit pattern, the client's own confirmation, and the irreversible-* lints.
lintIR emits irreversible-unenforced-policy (severity warning, token human-approval) for wanderlust.cancel and wanderlust.pay, and the cancel tool's advertised description says so (captured above).The test behind this
- negatives.test.ts#L335 — the test that pins "declared, not enforced"
- scenario-json.test.ts#L47 — the scenario table keeps this row locked and linked to its issue
Limits, lifecycle, errors, input and absence
S-11The fourth availability call in a minute is refused
wanderlust.availability is the one capability with a rate limit: 3 calls per 60 seconds, declared in a policy document. The fourth call in the window is refused with rate_limit_exceeded before the agency is asked; another capability still answers. On a Node runtime with a fake clock the test asserts the exact fourth refusal. On this live Worker the limit is approximate: the counter is a fixed window held in memory per isolate, so another isolate or a restart starts a new count, silently. The Worker says so on every response in the x-showcase-rate-limit header.
Declared: CDL and binding
# The only rate limit in the Showcase: 3 calls per minute per caller. A counter is supplied by
# whoever hosts the server; a capability that declares a rate limit and runs without one is refused.
apiVersion: archstone/v1
kind: Policy
metadata:
id: wanderlust-availability-rate
name: Availability rate limit
scope: capability
capabilityId: wanderlust.availability
spec:
rateLimit:
maxInvocations: 3
windowSeconds: 60
capability:
id: wanderlust.availability
description: "How many rooms a property has free on a date. Archstone's policy allows 3 calls per minute per caller and refuses the rest."
effect: read
input:
propertyId:
type: identifier
description: "The stay's id: the `id` field of a row returned by wanderlust_search (it looks like `ws-1001`), never the stay's name. To find a stay the user names, first call wanderlust_search with that name as `destination`."
date:
type: date
output:
availability:
type: Availability
lifecycle: beta
provider: wanderlust-api
bindings/wanderlust.availability.binding.yaml
# Implementation binding - how wanderlust.availability reaches the synthetic agency. NOT CDL.
binding:
capabilityId: wanderlust.availability
connector:
type: rest
rest:
baseUrl: "${SHOWCASE_API_URL}"
method: GET
path: /v1/availability
response:
resource: Availability
map:
propertyId: "$.propertyId"
date: "$.date"
roomsFree: "$.roomsFree"
pricePerNight: "$.pricePerNight"
wanderlust.Availability.resource.yaml
resource:
name: wanderlust.Availability
description: "How many rooms a property has free on a date."
fields:
propertyId:
type: identifier
description: "The property."
date:
type: date
description: "The date asked about."
roomsFree:
type: quantity
description: "Rooms free that night."
pricePerNight:
type: quantity
description: "Nightly rate in euro."
What the backend returned, and what the model received
on a path the manifest declaresreturned by the backend, not declared: never forwarded
{
"propertyId": "ws-1001",
"date": "2027-06-05",
"roomsFree": 3,
"pricePerNight": 118,
"net": 73.16,
"margin": 44.84,
"commission": 14.16
}
{
"availability": {
"propertyId": "ws-1001",
"date": "2027-06-05",
"roomsFree": 3,
"pricePerNight": 118
}
}
7 values in the backend's answer; 4 on declared paths; 3 never forwarded (net, margin, commission).
The call for S-11
curl -s -X POST https://demo.archstone.dev/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_availability","arguments":{"propertyId":"ws-1001","date":"2027-06-05"}}}'
{
"result": {
"content": [
{
"type": "text",
"text": "{\n \"availability\": {\n \"propertyId\": \"ws-1001\",\n \"date\": \"2027-06-05\",\n \"roomsFree\": 3,\n \"pricePerNight\": 118\n }\n}"
}
],
"structuredContent": {
"availability": {
"propertyId": "ws-1001",
"date": "2027-06-05",
"roomsFree": 3,
"pricePerNight": 118
}
},
"isError": false
},
"jsonrpc": "2.0",
"id": 1
}
What Archstone refused: the denial payload
The fourth call in the window
curl -s -X POST https://demo.archstone.dev/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_availability","arguments":{"propertyId":"ws-1001","date":"2027-06-05"}}}'
{
"result": {
"_meta": {
"dev.archstone/policy_denied": {
"error": "policy_denied",
"capability": "wanderlust.availability",
"reason": "rate_limit_exceeded"
}
},
"content": [
{
"type": "text",
"text": "capability 'wanderlust.availability' exceeded its rate limit (3 per 60s) for this caller."
}
],
"isError": true
},
"jsonrpc": "2.0",
"id": 1
}
Reason code rate_limit_exceeded under _meta["dev.archstone/policy_denied"]. The text content is the human-readable sentence; the client reads the reason from _meta.
x-showcase-backend-calls: 0). The margin fields the agency returns with every availability answer are not forwarded (N-11).Optional. Calls the public demo Worker from your browser; it answers pages on https://archstone.dev only. Nothing above depends on it.
The test behind this
- negatives.test.ts#L446 — N-11: the fourth call is denied rate_limit_exceeded
- manifests.test.ts#L289 — every live row runs end to end against the synthetic API
- denial-reasons.test.ts#L102 — the eight denial reasons, each reached through the real runtime
S-12Deprecated still works, with a note; retired is refused and never listed
tourism.search is lifecycle: deprecated: it still answers, in its original shape, and its advertised tool description carries the phasing-out note: the note lives in tools/list, not in the search result, and the live /run body returns it as evidence (the sentence of the description the card relies on, read from the same tool list). tourism.search-classic is lifecycle: retired: it is absent from tools/list and a call to it by name is refused with lifecycle_blocked, with no request to the agency.
Declared: CDL and binding
capability:
id: tourism.search
description: "Find places to stay matching a traveler's destination, dates, party and budget. Deprecated: it is being phased out; prefer wanderlust.search."
effect: read
input:
destination:
type: location
description: "The city name, e.g. Lisbon or Porto, optionally followed by its country after a comma; or the exact name of one stay, e.g. Pensão Azul, to look at that stay only."
dates:
type: date-range
description: "Check-in and check-out as ISO dates (YYYY-MM-DD), 1 to 30 nights. Work out calendar dates yourself; prose such as 'next weekend' is refused."
travelers:
type: party
budget:
type: money
required: false
description: "Maximum nightly rate as an object {amount, currency}, currency always \"EUR\" (a per-night ceiling, not a total for the stay). Not a bare number."
preferences:
type: preference-set
required: false
description: "Optional tags, all of which a stay must satisfy: pets (also pet-friendly, cat, dog), breakfast, family. Other tags are ignored."
output:
stays:
collection: Stay
totalMatches:
type: quantity
description: "Total number of stays the backend found, before this response's own page/limit."
lifecycle: deprecated
provider: wanderlust-api
# Implementation binding - how tourism.search reaches the synthetic agency. NOT CDL.
binding:
capabilityId: tourism.search
connector:
type: rest
rest:
baseUrl: "${SHOWCASE_API_URL}"
method: POST
path: /v1/search
response:
collection: "$.stays[*]"
resource: Stay
map:
name: "$.name"
location: "$.location"
pricePerNight: "$.pricePerNight"
rating: "$.rating"
extract:
totalMatches: "$.totalMatches"
# What the synthetic API last returned for the fixture's request (recorded: paths and types, never values).
# The shape includes fields this manifest never maps (net, margin, guests, ...): drift in them is still
# detected, and they are still never forwarded to a model.
# contract: the recorded shape of everything the backend answered (14 paths, most of them never mapped above) is in the file
tourism.search-classic.capability.yaml
capability:
id: tourism.search-classic
description: "The oldest search. Retired: switched off, neither listed nor callable."
effect: read
input:
destination:
type: location
dates:
type: date-range
travelers:
type: party
budget:
type: money
required: false
output:
stays:
collection: Stay
lifecycle: retired
provider: wanderlust-api
tourism.Stay.resource.yaml
# The unit the legacy tourism.search returns - identical to examples/manifests/tourism.
resource:
name: tourism.Stay
description: "A bookable place to stay matching a traveler's search."
fields:
name:
type: text
description: "The property's display name."
location:
type: location
description: "Where the stay is - city, region, or address."
pricePerNight:
type: quantity
description: "Nightly rate for the stay."
rating:
type: quantity
required: false
description: "Guest review score, when available."
What the backend returned, and what the model received
on a path the manifest declaresreturned by the backend, not declared: never forwarded
{
"stays": [
{
"id": "ws-1002",
"name": "Pensão Azul",
"location": "Lisbon",
"pricePerNight": 74,
"rating": 4.1,
"boardType": "ROOM_ONLY",
"freeCancellationUntil": "2026-08-03",
"roomDescription": "Family Room, two bedrooms",
"net": 57.72,
"commission": 8.88
},
{
"id": "ws-1004",
"name": "Rio Tejo Lofts",
"location": "Lisbon",
"pricePerNight": 96,
"rating": 4.3,
"boardType": "ROOM_ONLY",
"freeCancellationUntil": "2026-07-05",
"roomDescription": "Junior Suite, terrace",
"net": 74.88,
"commission": 11.52
},
{
"id": "ws-1001",
"name": "Casa Alfama",
"location": "Lisbon",
"pricePerNight": 118,
"rating": 4.6,
"boardType": "BREAKFAST",
"freeCancellationUntil": "2026-07-02",
"roomDescription": "Junior Suite, terrace",
"net": 92.04,
"commission": 14.16
},
{
"id": "ws-1003",
"name": "Miradouro Court",
"location": "Lisbon",
"pricePerNight": 139,
"rating": 4.8,
"boardType": "BREAKFAST",
"freeCancellationUntil": "2026-06-04",
"roomDescription": "Family Room, two bedrooms",
"net": 108.42,
"commission": 16.68
}
],
"totalMatches": 4
}
{
"stays": [
{
"name": "Pensão Azul",
"location": "Lisbon",
"pricePerNight": 74,
"rating": 4.1
},
{
"name": "Rio Tejo Lofts",
"location": "Lisbon",
"pricePerNight": 96,
"rating": 4.3
},
{
"name": "Casa Alfama",
"location": "Lisbon",
"pricePerNight": 118,
"rating": 4.6
},
{
"name": "Miradouro Court",
"location": "Lisbon",
"pricePerNight": 139,
"rating": 4.8
}
],
"totalMatches": 4
}
41 values in the backend's answer; 17 on declared paths; 24 never forwarded (id, boardType, freeCancellationUntil, roomDescription, net, commission).
The call for S-12
curl -s -X POST https://demo.archstone.dev/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"tourism_search","arguments":{"destination":"Lisbon","dates":{"from":"2027-05-12","to":"2027-05-15"},"travelers":{"adults":2}}}}'
{
"result": {
"content": [
{
"type": "text",
"text": "{\n \"stays\": [\n {\n \"name\": \"Pensão Azul\",\n \"location\": \"Lisbon\",\n \"pricePerNight\": 74,\n \"rating\": 4.1\n },\n {\n \"name\": \"Rio Tejo Lofts\",\n \"location\": \"Lisbon\",\n \"pricePerNight\": 96,\n \"rating\": 4.3\n },\n {\n \"name\": \"Casa Alfama\",\n \"location\": \"Lisbon\",\n \"pricePerNight\": 118,\n \"rating\": 4.6\n },\n {\n \"name\": \"Miradouro Court\",\n \"location\": \"Lisbon\",\n \"pricePerNight\": 139,\n \"rating\": 4.8\n }\n ],\n \"totalMatches\": 4\n}"
}
],
"structuredContent": {
"stays": [
{
"name": "Pensão Azul",
"location": "Lisbon",
"pricePerNight": 74,
"rating": 4.1
},
{
"name": "Rio Tejo Lofts",
"location": "Lisbon",
"pricePerNight": 96,
"rating": 4.3
},
{
"name": "Casa Alfama",
"location": "Lisbon",
"pricePerNight": 118,
"rating": 4.6
},
{
"name": "Miradouro Court",
"location": "Lisbon",
"pricePerNight": 139,
"rating": 4.8
}
],
"totalMatches": 4
},
"isError": false
},
"jsonrpc": "2.0",
"id": 1
}
{
"name": "tourism_search",
"description": "Find places to stay matching a traveler's destination, dates, party and budget. Deprecated: it is being phased out; prefer wanderlust.search. (deprecated — avoid new usage)",
"annotations": {
"readOnlyHint": true
}
}
Where the note lives (evidence of the live /run body, kind tool-description): the tool description in tools/list. The sentence, from the captured tools/list: Deprecated: it is being phased out; prefer wanderlust.search.
What Archstone refused: the denial payload
A call to the retired tool, by name
curl -s -X POST https://demo.archstone.dev/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"tourism_search-classic","arguments":{"destination":"Lisbon","dates":{"from":"2027-05-12","to":"2027-05-15"},"travelers":{"adults":2}}}}'
{
"result": {
"_meta": {
"dev.archstone/lifecycle_blocked": {
"error": "lifecycle_blocked",
"capability": "tourism.search-classic",
"lifecycle": "retired"
}
},
"content": [
{
"type": "text",
"text": "capability 'tourism.search-classic' is retired and can no longer be invoked."
}
],
"isError": true
},
"jsonrpc": "2.0",
"id": 1
}
Reason code lifecycle_blocked under _meta["dev.archstone/lifecycle_blocked"]; lifecycle: retired. The text content is the human-readable sentence; the client reads the reason from _meta.
Checked on the captured tools/list (11 tools): tourism_search-classic is not listed.
tools/list) and the agency receives nothing. The S-16 recording lists it as not exposed (retired), and the exposed total equals the tool list a client receives (N-12).Optional. Calls the public demo Worker from your browser; it answers pages on https://archstone.dev only. Nothing above depends on it.
The test behind this
- negatives.test.ts#L469 — N-12: deprecated works with a note; retired is refused and never listed
- manifests.test.ts#L289 — every live row runs end to end against the synthetic API
- denial-reasons.test.ts#L102 — the eight denial reasons, each reached through the real runtime
S-13An error row is a row; a wrong-typed price is a contract violation
wanderlust.room-status maps normal rows and, with onError, error rows that arrive inside a 2xx answer: the busy room is returned as a row marked $row: "error" with a declared code and message. A property whose price arrives in the wrong type (ws-1003: an object with a formatted string where a number is declared) is not passed on as fine: the result is an error with _meta["dev.archstone/contract_violation"] naming the capability and listing the field under invalid ({ field: "pricePerNight", expected: "quantity" }; missing stays empty, because the field was sent, in the wrong shape), no structured content, and a text that says the raw body is withheld. The live /run call makes both requests (alsoRun holds the second), so the card's two claims are each shown by its own run. The busy row and the wrong-typed price are one property and date each (ws-1002 and ws-1003 on 2027-05-12); every other property answers normally.
Declared: CDL and binding
capability:
id: wanderlust.room-status
description: "Is a property's room free on a date? Some rows may come back as errors when the agency's system is busy."
effect: read
input:
propertyId:
type: identifier
description: "The stay's id: the `id` field of a row returned by wanderlust_search (it looks like `ws-1001`), never the stay's name. To find a stay the user names, first call wanderlust_search with that name as `destination`."
date:
type: date
output:
rooms:
collection: RoomStatus
provider: wanderlust-api
# Implementation binding - how wanderlust.room-status reaches the synthetic agency. NOT CDL.
binding:
capabilityId: wanderlust.room-status
connector:
type: rest
rest:
baseUrl: "${SHOWCASE_API_URL}"
method: GET
path: /v1/room-status
response:
collection: "$.rows[*]"
resource: RoomStatus
map:
room: "$.room"
status: "$.status"
pricePerNight: "$.pricePerNight"
onError:
errorResource: RoomStatusError
when:
path: "$.error"
exists: true
map:
code: "$.error.code"
message: "$.error.message"
# What the synthetic API last returned for the fixture's request (recorded: paths and types, never values).
# The shape includes fields this manifest never maps (net, margin, guests, ...): drift in them is still
# detected, and they are still never forwarded to a model.
# contract: the recorded shape of everything the backend answered (8 paths, most of them never mapped above) is in the file
wanderlust.RoomStatus.resource.yaml
resource:
name: wanderlust.RoomStatus
description: "One room's status on a date."
fields:
room:
type: text
description: "Room type."
status:
type: enum
values: [free, taken]
description: "Whether it is free."
pricePerNight:
type: quantity
description: "Nightly rate in euro."
wanderlust.RoomStatusError.resource.yaml
resource:
name: wanderlust.RoomStatusError
description: "A row the agency could not answer, in place of a room status."
fields:
code:
type: text
description: "A short machine-readable reason."
message:
type: text
required: false
description: "What the agency said."
What the backend returned, and what the model received
on a path the manifest declaresreturned by the backend, not declared: never forwarded
{
"propertyId": "ws-1002",
"date": "2027-05-12",
"rows": [
{
"room": "Double Room",
"status": "free",
"pricePerNight": 74
},
{
"room": "Family Room",
"error": {
"code": "agency-busy",
"message": "The agency's room system is busy; try again shortly."
}
}
]
}
{
"rooms": [
{
"$row": "ok",
"room": "Double Room",
"status": "free",
"pricePerNight": 74
},
{
"$row": "error",
"code": "agency-busy",
"message": "The agency's room system is busy; try again shortly."
}
]
}
8 values in the backend's answer; 6 on declared paths; 2 never forwarded (propertyId, date).
The call for S-13
curl -s -X POST https://demo.archstone.dev/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_room-status","arguments":{"propertyId":"ws-1002","date":"2027-05-12"}}}'
{
"result": {
"content": [
{
"type": "text",
"text": "{\n \"rooms\": [\n {\n \"$row\": \"ok\",\n \"room\": \"Double Room\",\n \"status\": \"free\",\n \"pricePerNight\": 74\n },\n {\n \"$row\": \"error\",\n \"code\": \"agency-busy\",\n \"message\": \"The agency's room system is busy; try again shortly.\"\n }\n ]\n}"
}
],
"structuredContent": {
"rooms": [
{
"$row": "ok",
"room": "Double Room",
"status": "free",
"pricePerNight": 74
},
{
"$row": "error",
"code": "agency-busy",
"message": "The agency's room system is busy; try again shortly."
}
]
},
"isError": false
},
"jsonrpc": "2.0",
"id": 1
}
What Archstone refused: the denial payload
The wrong-format-price call: the property whose price is the wrong type (ws-1003)
curl -s -X POST https://demo.archstone.dev/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_room-status","arguments":{"propertyId":"ws-1003","date":"2027-05-12"}}}'
{
"result": {
"_meta": {
"dev.archstone/contract_violation": {
"error": "contract_violation",
"capability": "wanderlust.room-status",
"missing": [],
"invalid": [
{
"field": "pricePerNight",
"expected": "quantity"
}
]
}
},
"content": [
{
"type": "text",
"text": "contract violation: capability 'wanderlust.room-status' — provider response has a value of the wrong shape in field(s): pricePerNight (expected quantity). Declared output shape not met; raw body withheld."
}
],
"isError": true
},
"jsonrpc": "2.0",
"id": 1
}
Reason code contract_violation under _meta["dev.archstone/contract_violation"]; invalid: [{"field":"pricePerNight","expected":"quantity"}]. The text content is the human-readable sentence; the client reads the reason from _meta.
{
"propertyId": "ws-1003",
"date": "2027-05-12",
"rows": [
{
"room": "Double Room",
"status": "taken",
"pricePerNight": {
"amount": "139,00",
"currency": "EUR"
}
},
{
"room": "Family Room",
"status": "taken",
"pricePerNight": {
"amount": "202,00",
"currency": "EUR"
}
}
]
}
invalid, not missing. A failing backend is reported as an error with its status (backend returned 503), not as degraded: onError maps error rows inside a 2xx answer, and a non-2xx answer never reaches the response mapping. That 503 path is not reproducible on the live Worker; the test below asserts it (N-13).Optional. Calls the public demo Worker from your browser; it answers pages on https://archstone.dev only. Nothing above depends on it.
The test behind this
- negatives.test.ts#L528 — N-13: a failing backend is an error, a wrong-typed price is a violation
- manifests.test.ts#L289 — every live row runs end to end against the synthetic API
- denial-reasons.test.ts#L102 — the eight denial reasons, each reached through the real runtime
S-14Deletion is not a tool
The agency's API has DELETE /v1/guests/{name}/bookings, and it works with either demo key. No capability binds it: no capability, field, mapping or connector in the manifest names deletion. A client that asks for a tool called wanderlust_delete-guest-bookings is told it is unknown, and nothing reaches the agency.
Declared: CDL and binding
# capabilities.yaml - the Wanderlust Agency Showcase manifest (CDL 0.2).
#
# A SYNTHETIC agency: every hotel, guest, price and id is invented, and the backend it points at
# (../api/wanderlust-api.mjs) over-exposes on purpose. The manifest is the contract; what it leaves
# unnamed never reaches a model. See ../README.md.
#
# archstone apply examples/showcase/manifest
# SHOWCASE_API_URL=http://localhost:8788 archstone serve examples/showcase/manifest
company:
id: wanderlust
name: Wanderlust Agency (synthetic)
description: An invented travel agency that exercises every Archstone feature against a deliberately over-exposing backend.
capabilities:
- wanderlust.search
- wanderlust.stay-details
- wanderlust.stay-photos
- wanderlust.stay-page
- wanderlust.quote
- wanderlust.book
- wanderlust.cancel
- wanderlust.pay
- wanderlust.availability
- wanderlust.room-status
- wanderlust.neighbourhood
- tourism.search
- tourism.search-classic
providers:
- wanderlust-api
What the backend does
{
"guest": "Ana Pop",
"deletedBookings": 4,
"deletedAt": "2027-05-01T10:07:00.000Z"
}
The agency answers as though it deleted. The synthetic API stores nothing, so nothing is deleted; the point is that the route exists, works, and is bound by no capability.
The call for S-14
curl -s -X POST https://demo.archstone.dev/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_delete-guest-bookings","arguments":{"name":"Ana Pop"}}}'
{
"result": {
"content": [
{
"type": "text",
"text": "unknown tool: wanderlust_delete-guest-bookings"
}
],
"isError": true
},
"jsonrpc": "2.0",
"id": 1
}
Checked on the captured tools/list (11 tools): wanderlust_delete-guest-bookings, wanderlust_delete-booking, wanderlust_delete-bookings are not listed.
tools/list); the call is answered unknown tool and x-showcase-backend-calls is 0. This is absence by construction: there is no gate to bypass because there is no capability (N-14).Optional. Calls the public demo Worker from your browser; it answers pages on https://archstone.dev only. Nothing above depends on it.
The test behind this
- negatives.test.ts#L608 — N-14: deletion is not a tool
- tool-list.test.ts#L25 — the exposed tool list has no deletion tool
- manifests.test.ts#L289 — every live row runs end to end against the synthetic API
S-23A wrong-shaped argument is refused before the agency is asked
wanderlust.search declares its input: destination is a location (a string), dates a date-range (an object of ISO dates), travelers a party (an object of non-negative counts). The runtime validates the arguments against that declaration after the lifecycle and policy gates and before any connector work, and it coerces nothing. This scenario sends a destination that is an object ({"$ne": 1}, the shape of a query-injection attempt), the word tomorrow where a date range belongs, and a negative party. The call is refused with input_invalid and the agency is never called: x-showcase-backend-calls is 0.
Declared: CDL and binding
capability:
id: wanderlust.search
description: "Find places to stay matching a traveler's destination, dates, party and budget. Results come in the agency's own order; nothing here ranks them."
effect: read
input:
destination:
type: location
description: "The city name, e.g. Lisbon or Porto, optionally followed by its country after a comma; or the exact name of one stay, e.g. Casa Alfama or Pensão Azul, to look at that stay only and get its id. Use this whenever the user names a stay; do not ask for a city first."
dates:
type: date-range
description: "Check-in and check-out as ISO dates (YYYY-MM-DD), 1 to 30 nights. Work out calendar dates yourself; prose such as 'next weekend' is refused."
travelers:
type: party
budget:
type: money
required: false
description: "Maximum nightly rate as an object {amount, currency}, currency always \"EUR\" (a per-night ceiling, not a total for the stay). Not a bare number."
preferences:
type: preference-set
required: false
description: "Optional tags, all of which a stay must satisfy: pets (also pet-friendly, cat, dog), breakfast, family. Other tags are ignored."
output:
stays:
collection: Stay
totalMatches:
type: quantity
description: "How many stays the agency found."
provider: wanderlust-api
bindings/wanderlust.search.binding.yaml
# Implementation binding - how wanderlust.search reaches the synthetic agency. NOT CDL.
binding:
capabilityId: wanderlust.search
connector:
type: rest
rest:
baseUrl: "${SHOWCASE_API_URL}"
method: POST
path: /v1/stays/search
response:
collection: "$.stays[*]"
resource: Stay
map:
id: "$.id"
name: "$.name"
location: "$.location"
pricePerNight: "$.pricePerNight"
rating: "$.rating"
petPolicy: "$.petPolicy"
extract:
totalMatches: "$.totalMatches"
# What the synthetic API last returned for the fixture's request (recorded: paths and types, never values).
# The shape includes fields this manifest never maps (net, margin, guests, ...): drift in them is still
# detected, and they are still never forwarded to a model.
# contract: the recorded shape of everything the backend answered (18 paths, most of them never mapped above) is in the file
The call for S-23
curl -s -X POST https://demo.archstone.dev/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_search","arguments":{"destination":{"$ne":1},"dates":"tomorrow","travelers":-1}}}'
{
"result": {
"_meta": {
"dev.archstone/input_invalid": {
"error": "input_invalid",
"capability": "wanderlust.search",
"problems": [
{
"path": "destination",
"expected": "string"
},
{
"path": "dates",
"expected": "object"
},
{
"path": "travelers",
"expected": "object"
}
]
}
},
"content": [
{
"type": "text",
"text": "input for capability 'wanderlust.search' does not match its declared contract: destination (expected string); dates (expected object); travelers (expected object)."
}
],
"isError": true
},
"jsonrpc": "2.0",
"id": 1
}
Reason code input_invalid under _meta["dev.archstone/input_invalid"]; problems: [{"path":"destination","expected":"string"},{"path":"dates","expected":"object"},{"path":"travelers","expected":"object"}]. The text content is the human-readable sentence; the client reads the reason from _meta.
destination expected string, dates and travelers expected object, at most 16 problems); it never contains a value that was sent or the name of an undeclared key (N-23). The same arguments made well-formed reach the agency, which is the test's control. This holds because the declared input is enforced (core #195); before that fix the arguments were forwarded and only the agency's own validation stopped them.Optional. Calls the public demo Worker from your browser; it answers pages on https://archstone.dev only. Nothing above depends on it.
The test behind this
- negatives.test.ts#L628 — N-23: refused with its three problems, the agency receives nothing, no value echoed
- manifests.test.ts#L289 — every live row runs end to end against the synthetic API
Recorded: what runs only on a machine of your own
S-15A report from the agency's own database (local only)
The SQL provider is Node-only and needs a database, so this scenario is a recording, not a live call. The manifest under local/reporting is not part of the manifest the Worker serves. The capability is business-only (what an analyst can ask, what comes back); the binding holds the query, and no tenant filter: whose bookings these are is the caller's identity, enforced by the database (row-level security), not by the binding.
Declared
# Business only: what an analyst can ask and what comes back. No table, no query, no tenant. Whose
# bookings these are is the caller's identity, enforced by the database (see fixture.sql).
capability:
id: wanderlust.bookings-by-city
description: How many bookings the caller's own agency took in each city in one month.
effect: read
input:
month:
type: text
description: The month, as YYYY-MM.
output:
cities:
collection: CityBookings
provider: agency-warehouse
# NOT CDL - implementation (how). The connection string is an env placeholder, never a literal.
#
# What is NOT here: any tenant. The query does not filter by whose bookings these are, and the
# grammar has no way to - the caller's identity reaches the database as a transaction-scoped setting
# that a row-level-security policy reads. Drop the policy and `archstone verify` goes red.
binding:
capabilityId: wanderlust.bookings-by-city
connector:
type: sql
sql:
engine: postgres
dsn: "${REPORTING_DSN}"
statementKind: select
query: |
SELECT city, bookings
FROM app.bookings_by_city_v
WHERE month = $1
ORDER BY bookings DESC, city
params:
- month
# Row -> CityBookings. Anything the view returns and this does not name is dropped here.
response:
collection: "$[*]"
resource: CityBookings
map:
city: "$.city"
bookings: "$.bookings"
# contract: the recorded shape of everything the backend answered (4 paths, most of them never mapped above) is in the file
local/reporting/wanderlust.CityBookings.resource.yaml
resource:
name: wanderlust.CityBookings
description: The number of bookings one agency took in one city in one month.
fields:
city:
type: text
description: The city.
bookings:
type: quantity
description: How many bookings.
Recording
Recorded with the core's recorder, CLI stamp 0.31.0 (the version when the content last changed), with a fixed clock (2027-05-01); machine-specific text is rewritten to placeholders (none needed). The file is vendored verbatim from the pinned commit: transcripts/s-15.json.
$ archstone apply examples/showcase/local/reportingexit 0
archstone apply examples/showcase/local/reporting
company Wanderlust Agency (synthetic) (wanderlust)
providers agency-warehouse
declared 1 capabilities
loaded 1 capability docs, 1 bindings
✓ wanderlust.bookings-by-city [read] → agency-warehouse
✓ shapes valid
semantic 0 error(s), 0 warning(s)
registry IR v0 — 1 capabilities, 1 invocable (bound)
→ run 'archstone serve examples/showcase/local/reporting' to expose 1 tool(s) to an AI agent over MCP
$ archstone verify examples/showcase/local/reporting --identity-map examples/showcase/local/reporting/identity-map.jsonexit 0
archstone verify examples/showcase/local/reporting
🟢 wanderlust.bookings-by-city — fingerprint unchanged, mapping OK
call wanderlust_bookings-by-citydemo:analyst
{
"month": "2027-05"
}
{
"as": "demo:analyst",
"isError": false,
"structuredContent": {
"cities": [
{
"bookings": 5,
"city": "Lisbon"
},
{
"bookings": 3,
"city": "Porto"
},
{
"bookings": 2,
"city": "Seville"
}
]
}
}
call wanderlust_bookings-by-citydemo:outsider
{
"month": "2027-05"
}
{
"as": "demo:outsider",
"isError": false,
"structuredContent": {
"cities": []
}
}
call wanderlust_bookings-by-city(no caller)
{
"month": "2027-05"
}
{
"as": "(no caller)",
"isError": true
}
What the recorder checked
- `apply` compiles the local reporting manifest offline (exit 0)
- `verify` exits 0: the recorded contract holds
- the local manifest builds a registry with one tool
- the analyst gets a table of bookings per city for the month: Lisbon 5, Porto 3, Seville 2
- the reporting manifest lives outside the live one
What Archstone kept out, or did not do
- `verify` replays the request as another tenant (the fixture names one) and is green only because that replay returned zero rows
- the table has exactly the declared columns, city and bookings (the guest passport column is not in the view)
- another agency's analyst asking the same question gets no rows: the database's row-level security answers, not the binding
- a call with no caller identity is refused, not answered
- the live manifest's tool list has no bookings-by-city tool
- no capability in the live manifest is bound to sql
- no file under the live manifest declares a sql connector
The test behind this
- recorded-s15.test.ts#L15 — recorded S-15: the table, isolation replayed, the live manifest has no sql tool
S-16What the AI can see, in one list
archstone apply --exposure prints, per capability, what a model sends, what it is shown and what it never sees: the declared fields on one side, and on the other the paths the recorded contract observed the backend returning that the manifest does not name. The recording below is the full report for the 13-capability manifest.
Recording
Recorded with the core's recorder, CLI stamp 0.32.0 (the version when the content last changed), with a fixed clock (2027-05-01); machine-specific text is rewritten to placeholders (none needed). The file is vendored verbatim from the pinned commit: transcripts/s-16.json.
$ archstone apply examples/showcase/manifest --exposureexit 0
stdout, 106 lines: show
archstone apply examples/showcase/manifest
company Wanderlust Agency (synthetic) (wanderlust)
providers wanderlust-api
declared 13 capabilities
loaded 13 capability docs, 13 bindings
✓ tourism.search-classic [read] → wanderlust-api
✓ tourism.search [read] → wanderlust-api
✓ wanderlust.availability [read] → wanderlust-api
✓ wanderlust.book [write] → wanderlust-api
✓ wanderlust.cancel [irreversible] → wanderlust-api
✓ wanderlust.neighbourhood [read] → wanderlust-api
✓ wanderlust.pay [irreversible] → wanderlust-api
✓ wanderlust.quote [write] → wanderlust-api
✓ wanderlust.room-status [read] → wanderlust-api
✓ wanderlust.search [read] → wanderlust-api
✓ wanderlust.stay-details [read] → wanderlust-api
✓ wanderlust.stay-page [read] → wanderlust-api
✓ wanderlust.stay-photos [read] → wanderlust-api
policies 4 policy document(s)
✓ wanderlust-availability-rate → capability wanderlust.availability
✓ wanderlust-book-principals → capability wanderlust.book
✓ wanderlust-cancel-principals → capability wanderlust.cancel
✓ wanderlust-pay-principals → capability wanderlust.pay
✓ shapes valid
semantic 0 error(s), 2 warning(s)
⚠ capability 'wanderlust.cancel' is irreversible and declares policies:[human-approval], which this version does not enforce: no approval mechanism exists. An agent can run it without anyone approving. Put the approval step in the provider, or do not serve this capability to an agent unattended.
⚠ capability 'wanderlust.pay' is irreversible and declares policies:[human-approval], which this version does not enforce: no approval mechanism exists. An agent can run it without anyone approving. Put the approval step in the provider, or do not serve this capability to an agent unattended.
registry IR v0 — 13 capabilities, 13 invocable (bound)
exposure what a model sends, is shown, and never sees — names and types only
totals 13 declared — 11 exposed, 1 unlisted, 1 not exposed
tourism.search [read] exposed
receives budget (money, optional), dates (date-range), destination (location), preferences (preference-set, optional), travelers (party)
exposes stays[].location (location), stays[].name (text), stays[].pricePerNight (quantity), stays[].rating (quantity, optional), totalMatches (quantity, extract)
withholds $.stays[].boardType (string), $.stays[].commission (number), $.stays[].freeCancellationUntil (string), $.stays[].id (string), $.stays[].net (number), $.stays[].roomDescription (string)
as observed in fixtures/tourism.search.golden.json (sha256:f5475f…)
tourism.search-classic [read] not exposed (retired)
receives budget (money, optional), dates (date-range), destination (location), travelers (party)
exposes stays[].location (location), stays[].name (text), stays[].pricePerNight (quantity), stays[].rating (quantity, optional)
withholds unknown — no recorded contract
wanderlust.availability [read] exposed
receives date (date), propertyId (identifier)
exposes availability.date (date), availability.pricePerNight (quantity), availability.propertyId (identifier), availability.roomsFree (quantity)
withholds unknown — no recorded contract
wanderlust.book [write] exposed
receives dates (date-range), guestName (string), quoteId (identifier), stayId (identifier), travelers (party)
exposes booking.bookingId (identifier), booking.dates (date-range), booking.payBy (datetime), booking.paymentQuote (identifier), booking.petFee (money), booking.pets (quantity), booking.status (enum), booking.stayId (identifier), booking.total (money)
withholds unknown — no recorded contract
wanderlust.cancel [irreversible] exposed
receives bookingId (identifier)
exposes cancellation.bookingId (identifier), cancellation.cancelledAt (datetime), cancellation.refund (money), cancellation.status (enum)
withholds unknown — no recorded contract
wanderlust.neighbourhood [read] unlisted (experimental) — callable by id, not advertised
receives area (text)
exposes neighbourhood.area (text), neighbourhood.summary (text), neighbourhood.walkability (quantity, optional)
withholds $.localTips (array), $.localTips[] (string)
as observed in fixtures/wanderlust.neighbourhood.golden.json (sha256:fa7430…)
wanderlust.pay [irreversible] exposed
receives amount (money), bookingId (identifier), paymentQuote (identifier)
exposes payment.amount (money), payment.bookingId (identifier), payment.paidAt (datetime), payment.paymentId (identifier), payment.status (enum)
withholds unknown — no recorded contract
wanderlust.quote [write] exposed
receives dates (date-range), pets (quantity, optional), stayId (identifier), travelers (party)
exposes quote.dates (date-range), quote.expiresAt (datetime), quote.nights (quantity), quote.petFee (money), quote.pets (quantity), quote.quoteId (identifier), quote.stayId (identifier), quote.total (money)
withholds unknown — no recorded contract
wanderlust.room-status [read] exposed
receives date (date), propertyId (identifier)
exposes rooms[].code (text), rooms[].message (text, optional), rooms[].pricePerNight (quantity), rooms[].room (text), rooms[].status (enum)
withholds $.date (string), $.propertyId (string)
as observed in fixtures/wanderlust.room-status.golden.json (sha256:d00727…)
wanderlust.search [read] exposed
receives budget (money, optional), dates (date-range), destination (location), preferences (preference-set, optional), travelers (party)
exposes stays[].id (identifier), stays[].location (location), stays[].name (text), stays[].petPolicy (text, optional), stays[].pricePerNight (quantity), stays[].rating (quantity, optional), totalMatches (quantity, extract)
withholds $.stays[].commission (number), $.stays[].currency (string), $.stays[].description_html (string), $.stays[].hostContact (object), $.stays[].hostContact.name (string), $.stays[].hostContact.phone (string), $.stays[].margin (number), $.stays[].net (number)
as observed in fixtures/wanderlust.search.golden.json (sha256:243302…)
wanderlust.stay-details [read] exposed
receives stayId (identifier)
exposes stay.location (location), stay.name (text), stay.petPolicy (text, optional), stay.pricePerNight (quantity), stay.rating (quantity, optional), stay.rooms (wanderlust.Room[])
withholds $.commission (number), $.currency (string), $.description_html (string), $.guests (array), $.guests[] (object), $.guests[].email (string), $.guests[].name (string), $.guests[].passport (string), $.guests[].phone (string), $.id (string), $.margin (number), $.net (number)
as observed in fixtures/wanderlust.stay-details.golden.json (sha256:1ed3fe…)
wanderlust.stay-page [read] exposed
receives stayId (identifier)
exposes pages[].name (text), pages[].url (web-page, optional)
withholds unknown — no recorded contract
wanderlust.stay-photos [read] exposed
receives stayId (identifier)
exposes gallery.photos (image[])
withholds unknown — no recorded contract
→ run 'archstone serve examples/showcase/manifest' to expose 13 tool(s) to an AI agent over MCP
What the recorder checked
- `apply --exposure` exits 0 on the live manifest
- the same report as JSON exits 0
- the report covers all 13 capabilities
- the exposed list is not empty (the check below is not vacuous)
- `build` exits 0 on the live manifest
- the manifest declares at least 30 resource fields in total
- the backend was observed returning 'margin' (listed under withholds, never exposed)
- the backend was observed returning 'passport' (listed under withholds, never exposed)
- the backend was observed returning 'phone' (listed under withholds, never exposed)
- the backend was observed returning 'description_html' (listed under withholds, never exposed)
What Archstone kept out, or did not do
- no exposed field is named or nested under 'margin'
- no exposed field is named or nested under 'passport'
- no exposed field is named or nested under 'phone'
- no exposed field is named or nested under 'description_html'
- no capability in the list deletes anything
- no resource declares a field named 'margin'
- no resource declares a field named 'passport'
- no resource declares a field named 'phone'
- no resource declares a field named 'description_html'
- no outbound connection was attempted
The same question, asked of the published CLI
The transcript above was recorded with the core's recorder (CLI stamp 0.32.0). As a cross-check, the page's check runs archstone apply --exposure --json with the published @archstone/cli 0.33.0 on the same manifest: 13 capabilities, 63 exposed paths, and none of them is named margin, passport, phone, description_html, net, commission, guests, email. For wanderlust.search it exposes stays[].id, stays[].location, stays[].name, stays[].petPolicy, stays[].pricePerNight, stays[].rating, totalMatches and lists as observed in the backend's answer but not exposed:
$.stays[].commission
$.stays[].currency
$.stays[].description_html
$.stays[].hostContact
$.stays[].hostContact.name
$.stays[].hostContact.phone
$.stays[].margin
$.stays[].net
The test behind this
- recorded-s16.test.ts#L20 — recorded S-16: nothing named margin, passport, phone or description_html is exposed; no capability deletes
S-17The agency added a field: verify names it, adopt needs a person
The recorder wraps the synthetic API in its own server to add a guestEmail field (no flag exists on the API itself). archstone verify replays the recorded contract against the backend and names the gained field; archstone adopt is the interactive step where a person decides to declare it. With nobody at the keyboard it writes nothing and exits non-zero. The typed answers of the person (typed) are part of the recording because they are where the decision lives.
Recording
Recorded with the core's recorder, CLI stamp 0.32.0 (the version when the content last changed), with a fixed clock (2027-05-01); machine-specific text is rewritten to placeholders (<manifest>). The file is vendored verbatim from the pinned commit: transcripts/s-17.json.
$ archstone verify <manifest>exit 0
archstone verify <manifest>
🟢 tourism.search — fingerprint unchanged, mapping OK
🟢 wanderlust.neighbourhood — fingerprint unchanged, mapping OK
🟢 wanderlust.room-status — fingerprint unchanged, mapping OK
🟢 wanderlust.search — fingerprint unchanged, mapping OK
🟢 wanderlust.stay-details — fingerprint unchanged, mapping OK
nested keys not declared (dropped): rooms.amenities.host, rooms.amenities.lastUsedBy, rooms.amenities.history, rooms.id, rooms.net, rooms.margin, rooms.commission, rooms.guests
$ archstone verify <manifest>exit 0
archstone verify <manifest>
🟢 tourism.search — fingerprint unchanged, mapping OK
🟢 wanderlust.neighbourhood — fingerprint unchanged, mapping OK
🟢 wanderlust.room-status — fingerprint unchanged, mapping OK
🟢 wanderlust.search — fingerprint unchanged, mapping OK
🟡 wanderlust.stay-details — mapping still resolves; response shape gained 1 field(s): $.guestEmail (string)
nested keys not declared (dropped): rooms.amenities.host, rooms.amenities.lastUsedBy, rooms.amenities.history, rooms.id, rooms.net, rooms.margin, rooms.commission, rooms.guests
$ archstone adopt <manifest>exit 1
archstone adopt <manifest>
tourism.search — nothing to adopt.
wanderlust.neighbourhood — nothing to adopt.
wanderlust.room-status — nothing to adopt.
wanderlust.search — nothing to adopt.
wanderlust.stay-details — 1 field(s) the backend returns and the manifest does not declare:
· $.guestEmail (string) → guestEmail: text
archstone adopt: no more input — nothing written. Adoption needs a person (PromptUnavailable).
$ archstone adopt <manifest>exit 0
archstone adopt <manifest>
tourism.search — nothing to adopt.
wanderlust.neighbourhood — nothing to adopt.
wanderlust.room-status — nothing to adopt.
wanderlust.search — nothing to adopt.
wanderlust.stay-details — 1 field(s) the backend returns and the manifest does not declare:
· $.guestEmail (string) → guestEmail: text
Declare guestEmail (text)? [y/N] Describe guestEmail — an agent reads this to decide whether to use it:
>
wanderlust.stay-details — declared guestEmail; contract re-recorded.
$ archstone verify <manifest>exit 0
archstone verify <manifest>
🟢 tourism.search — fingerprint unchanged, mapping OK
🟢 wanderlust.neighbourhood — fingerprint unchanged, mapping OK
🟢 wanderlust.room-status — fingerprint unchanged, mapping OK
🟢 wanderlust.search — fingerprint unchanged, mapping OK
🟢 wanderlust.stay-details — fingerprint unchanged, mapping OK
nested keys not declared (dropped): rooms.amenities.host, rooms.amenities.lastUsedBy, rooms.amenities.history, rooms.id, rooms.net, rooms.margin, rooms.commission, rooms.guests
What the recorder checked
- before the change, `verify` exits 0 (the contract holds)
- strict verify (exit non-zero on a gained field): not available in this version - #178
- after the change, `verify` names the new field: a yellow reading on wanderlust.stay-details, "gained 1 field(s): $.guestEmail"
- and the other capabilities' readings did not change (no other line is yellow or red)
- `adopt` with nobody at the keyboard exits non-zero
- `adopt` with a person's answers exits 0
- it reports the field declared and the contract re-recorded
- the field is in the exposure now that a person declared it
- `verify` exits 0 again once the field is declared
What Archstone kept out, or did not do
- `verify` is a read-only check: the manifest is byte-identical after it
- the field is absent from the exposure while undeclared
- `adopt` with nobody at the keyboard writes nothing
- the field is still absent from the exposure
One part of this scenario is locked; the rest above is a real recording.
Known deviation
In this version a gained field is a yellow reading: verify names it and exits 0. Only a lost field, a changed type or a missing required value are red and exit 1. An opt-in strict mode that exits non-zero on a gained field is not available; it is tracked in NousVigil/archstone#178. The transcript says so in a claim. Separately, adopt used to resolve a resource by its bare name, and this manifest has two resources called Stay (#177, fixed in 0.32.0); the recording predates the fix and still runs adopt on wanderlust.stay-details.
The test behind this
- recorded-s17.test.ts#L17 — recorded S-17: verify names the field, adopt needs a person, the field is absent until declared
S-18What changed between two declarations, with the backend stopped
archstone diff compares two declarations, not backends. It names one added visible field (wanderlust.Stay.rating) and one added action (wanderlust.quote) and classifies each as compatible; the recorder runs it with the backend stopped and checks that it made no connection.
Recording
Recorded with the core's recorder, CLI stamp 0.32.0 (the version when the content last changed), with a fixed clock (2027-05-01); machine-specific text is rewritten to placeholders (<before>). The file is vendored verbatim from the pinned commit: transcripts/s-18.json.
$ archstone diff <before> examples/showcase/manifest --allexit 0
archstone diff compares declarations, not backends — run 'archstone verify' for the backend.
before <before> (wanderlust)
after examples/showcase/manifest (wanderlust)
compatible wanderlust.Stay — field 'rating' added (optional, quantity) (reaches wanderlust.search)
compatible wanderlust.quote — capability added (write)
compatible wanderlust.search — binding response mapping changed
0 breaking, 0 notable, 3 compatible — exit 0
What the recorder checked
- the earlier declaration is itself a valid manifest
- `diff` exits 0: nothing in the change is breaking
- it names exactly one added visible field: wanderlust.Stay.rating
- it names exactly one added action: wanderlust.quote
What Archstone kept out, or did not do
- it reports no breaking change
- the backend is stopped: the address the commands were given refuses a connection
- no outbound connection was attempted by `diff`
The test behind this
- recorded-s18.test.ts#L9 — recorded S-18: one added field, one added action, no connection
S-19audit and doctor, sending nothing anywhere
The recorder runs five real executions through the embedded SDK (search, quote, book, cancel, pay) into an audit trail, then runs archstone doctor, archstone audit (as a table and as CSV) and apply --exposure. audit lists what ran, including the two irreversible actions; the exposed fields in the recording come from apply --exposure, not from audit. The commands run under a guard that makes any socket, DNS lookup or fetch fail and be logged, so "zero outbound requests" is checked on the process, not only on the backend's counter.
Recording
Recorded with the core's recorder, CLI stamp 0.32.0 (the version when the content last changed), with a fixed clock (2027-05-01); machine-specific text is rewritten to placeholders (<work>). The file is vendored verbatim from the pinned commit: transcripts/s-19.json.
call wanderlust.searchundefined
{
"dates": {
"from": "2027-05-12",
"to": "2027-05-15"
},
"destination": "Lisbon",
"travelers": {
"adults": 2
}
}
{
"status": "ok"
}
call wanderlust.quoteundefined
{
"dates": {
"from": "2027-05-12",
"to": "2027-05-15"
},
"stayId": "ws-1001",
"travelers": {
"adults": 2
}
}
{
"status": "ok"
}
call wanderlust.bookundefined
{
"dates": {
"from": "2027-05-12",
"to": "2027-05-15"
},
"guestName": "Ana Pop",
"quoteId": "Q-n34aqj6o-8902ed05",
"stayId": "ws-1001",
"travelers": {
"adults": 2
}
}
{
"status": "ok"
}
call wanderlust.cancelundefined
{
"bookingId": "B-0000cafe"
}
{
"status": "ok"
}
call wanderlust.payundefined
{
"amount": {
"amount": 354,
"currency": "EUR"
},
"bookingId": "B-0162f95c1506",
"paymentQuote": "PQ-n34aqj6o-61e32b76"
}
{
"status": "ok"
}
$ archstone doctor examples/showcase/manifestexit 0
stdout, 124 lines: show
archstone doctor examples/showcase/manifest
🟡 tourism.search-classic — bound, but records no contract fixture
`archstone verify` replays a recorded fixture against the live backend. With no fixture there is nothing to replay, so backend drift is found by an agent, in front of a customer, instead of by CI.
(no-contract)
🟡 wanderlust.availability — bound, but records no contract fixture
`archstone verify` replays a recorded fixture against the live backend. With no fixture there is nothing to replay, so backend drift is found by an agent, in front of a customer, instead of by CI.
(no-contract)
🟡 wanderlust.cancel — is irreversible and declares policies:[human-approval], which this version does not enforce: no approval mechanism exists.
An agent can run it without anyone approving. Put the approval step in the provider, or do not serve this capability to an agent unattended.
(irreversible-unenforced-policy)
🟡 wanderlust.pay — is irreversible and declares policies:[human-approval], which this version does not enforce: no approval mechanism exists.
An agent can run it without anyone approving. Put the approval step in the provider, or do not serve this capability to an agent unattended.
(irreversible-unenforced-policy)
🟡 wanderlust.stay-page — bound, but records no contract fixture
`archstone verify` replays a recorded fixture against the live backend. With no fixture there is nothing to replay, so backend drift is found by an agent, in front of a customer, instead of by CI.
(no-contract)
🟡 wanderlust.stay-photos — bound, but records no contract fixture
`archstone verify` replays a recorded fixture against the live backend. With no fixture there is nothing to replay, so backend drift is found by an agent, in front of a customer, instead of by CI.
(no-contract)
🔵 tourism.search-classic — baseUrl comes from the environment (${SHOWCASE_API_URL})
Nothing wrong with it — but the deployment, not the manifest, decides where this capability points. Confirm the variable is set to the intended backend in every environment that runs it.
(env-baseurl)
🔵 tourism.search — baseUrl comes from the environment (${SHOWCASE_API_URL})
Nothing wrong with it — but the deployment, not the manifest, decides where this capability points. Confirm the variable is set to the intended backend in every environment that runs it.
(env-baseurl)
🔵 wanderlust.availability — baseUrl comes from the environment (${SHOWCASE_API_URL})
Nothing wrong with it — but the deployment, not the manifest, decides where this capability points. Confirm the variable is set to the intended backend in every environment that runs it.
(env-baseurl)
🔵 wanderlust.availability — declares a rate limit, which needs a counter supplied at runtime
With no counter the call is denied, fail-closed, at the first invocation. On more than one instance the counter must be shared, or a declared 100/min becomes 100/min per instance.
(ratelimit-needs-counter)
🔵 wanderlust.book — bound and `write`, so it records no contract fixture — and should not
`archstone verify` replays a recorded fixture as a real invocation, so a fixture here would repeat this capability's effect against the live backend on every CI run. `verify` skips it by default for that reason. Where this capability has a `read` counterpart, cover the drift with that instead — the quote half of a quote → commit pair hits the same host, auth and serialization at zero risk. Not every write has one, and Archstone cannot tell you which capability it is: nothing in CDL declares that relationship. Only if this binding's `${VAR}` genuinely resolves to a sandbox tenant is recording one worthwhile, replayed with `archstone verify --sandbox`: the flag re-includes the binding, it does not make the backend safe.
(no-contract-non-read)
🔵 wanderlust.book — baseUrl comes from the environment (${SHOWCASE_API_URL})
Nothing wrong with it — but the deployment, not the manifest, decides where this capability points. Confirm the variable is set to the intended backend in every environment that runs it.
(env-baseurl)
🔵 wanderlust.book — requires an authenticated caller
The surface serving it must carry a per-request principal — `resolveCaller` on HTTP or the embedded SDK. `archstone serve` (stdio) has one static caller for the whole process, so it cannot serve this capability to more than one identity.
(authenticated-needs-principal)
🔵 wanderlust.cancel — bound and `irreversible`, so it records no contract fixture — and should not
`archstone verify` replays a recorded fixture as a real invocation, so a fixture here would repeat this capability's effect against the live backend on every CI run. `verify` skips it by default for that reason. Where this capability has a `read` counterpart, cover the drift with that instead — the quote half of a quote → commit pair hits the same host, auth and serialization at zero risk. Not every write has one, and Archstone cannot tell you which capability it is: nothing in CDL declares that relationship. Only if this binding's `${VAR}` genuinely resolves to a sandbox tenant is recording one worthwhile, replayed with `archstone verify --sandbox`: the flag re-includes the binding, it does not make the backend safe.
(no-contract-non-read)
🔵 wanderlust.cancel — baseUrl comes from the environment (${SHOWCASE_API_URL})
Nothing wrong with it — but the deployment, not the manifest, decides where this capability points. Confirm the variable is set to the intended backend in every environment that runs it.
(env-baseurl)
🔵 wanderlust.cancel — is declared `irreversible`
No API description states this, so it was a human judgement: an agent must confirm explicitly and must never auto-retry. Re-read it before go-live — `irreversible` is the difference between looking up a price and charging a card. `archstone verify` applies the same judgement: it will not replay this capability's fixture against the live backend unless you assert a sandbox with --sandbox.
(irreversible-effect)
🔵 wanderlust.cancel — requires an authenticated caller
The surface serving it must carry a per-request principal — `resolveCaller` on HTTP or the embedded SDK. `archstone serve` (stdio) has one static caller for the whole process, so it cannot serve this capability to more than one identity.
(authenticated-needs-principal)
🔵 wanderlust.neighbourhood — baseUrl comes from the environment (${SHOWCASE_API_URL})
Nothing wrong with it — but the deployment, not the manifest, decides where this capability points. Confirm the variable is set to the intended backend in every environment that runs it.
(env-baseurl)
🔵 wanderlust.neighbourhood — is `experimental`: hidden from tool listings but still invocable by id
Deliberate behaviour, and easy to forget: an agent that knows the id can still call it. Confirm that is what you want in production.
(experimental-capability)
🔵 wanderlust.pay — bound and `irreversible`, so it records no contract fixture — and should not
`archstone verify` replays a recorded fixture as a real invocation, so a fixture here would repeat this capability's effect against the live backend on every CI run. `verify` skips it by default for that reason. Where this capability has a `read` counterpart, cover the drift with that instead — the quote half of a quote → commit pair hits the same host, auth and serialization at zero risk. Not every write has one, and Archstone cannot tell you which capability it is: nothing in CDL declares that relationship. Only if this binding's `${VAR}` genuinely resolves to a sandbox tenant is recording one worthwhile, replayed with `archstone verify --sandbox`: the flag re-includes the binding, it does not make the backend safe.
(no-contract-non-read)
🔵 wanderlust.pay — baseUrl comes from the environment (${SHOWCASE_API_URL})
Nothing wrong with it — but the deployment, not the manifest, decides where this capability points. Confirm the variable is set to the intended backend in every environment that runs it.
(env-baseurl)
🔵 wanderlust.pay — is declared `irreversible`
No API description states this, so it was a human judgement: an agent must confirm explicitly and must never auto-retry. Re-read it before go-live — `irreversible` is the difference between looking up a price and charging a card. `archstone verify` applies the same judgement: it will not replay this capability's fixture against the live backend unless you assert a sandbox with --sandbox.
(irreversible-effect)
🔵 wanderlust.pay — requires an authenticated caller
The surface serving it must carry a per-request principal — `resolveCaller` on HTTP or the embedded SDK. `archstone serve` (stdio) has one static caller for the whole process, so it cannot serve this capability to more than one identity.
(authenticated-needs-principal)
🔵 wanderlust.quote — bound and `write`, so it records no contract fixture — and should not
`archstone verify` replays a recorded fixture as a real invocation, so a fixture here would repeat this capability's effect against the live backend on every CI run. `verify` skips it by default for that reason. Where this capability has a `read` counterpart, cover the drift with that instead — the quote half of a quote → commit pair hits the same host, auth and serialization at zero risk. Not every write has one, and Archstone cannot tell you which capability it is: nothing in CDL declares that relationship. Only if this binding's `${VAR}` genuinely resolves to a sandbox tenant is recording one worthwhile, replayed with `archstone verify --sandbox`: the flag re-includes the binding, it does not make the backend safe.
(no-contract-non-read)
🔵 wanderlust.quote — baseUrl comes from the environment (${SHOWCASE_API_URL})
Nothing wrong with it — but the deployment, not the manifest, decides where this capability points. Confirm the variable is set to the intended backend in every environment that runs it.
(env-baseurl)
🔵 wanderlust.room-status — baseUrl comes from the environment (${SHOWCASE_API_URL})
Nothing wrong with it — but the deployment, not the manifest, decides where this capability points. Confirm the variable is set to the intended backend in every environment that runs it.
(env-baseurl)
🔵 wanderlust.search — baseUrl comes from the environment (${SHOWCASE_API_URL})
Nothing wrong with it — but the deployment, not the manifest, decides where this capability points. Confirm the variable is set to the intended backend in every environment that runs it.
(env-baseurl)
🔵 wanderlust.stay-details — baseUrl comes from the environment (${SHOWCASE_API_URL})
Nothing wrong with it — but the deployment, not the manifest, decides where this capability points. Confirm the variable is set to the intended backend in every environment that runs it.
(env-baseurl)
🔵 wanderlust.stay-page — baseUrl comes from the environment (${SHOWCASE_API_URL})
Nothing wrong with it — but the deployment, not the manifest, decides where this capability points. Confirm the variable is set to the intended backend in every environment that runs it.
(env-baseurl)
🔵 wanderlust.stay-photos — baseUrl comes from the environment (${SHOWCASE_API_URL})
Nothing wrong with it — but the deployment, not the manifest, decides where this capability points. Confirm the variable is set to the intended backend in every environment that runs it.
(env-baseurl)
13 capabilities checked — 0 error · 6 warning · 24 advisory.
$ archstone audit <work>/trail.jsonlexit 0
5 records
from 2027-05-01T10:07:00.000Z
to 2027-05-01T10:07:04.000Z
By outcome
succeeded 5
By capability
wanderlust.book 1
wanderlust.cancel 1
wanderlust.pay 1
wanderlust.quote 1
wanderlust.search 1
By principal
demo:visitor 5
$ archstone audit <work>/trail.jsonl --format csvexit 0
startedAt,completedAt,capabilityId,provider,phase,denialReason,principal,consumer,policyRuleIds,sessionId,id
2027-05-01T10:07:00.000Z,2027-05-01T10:07:00.001Z,wanderlust.search,wanderlust-api,succeeded,,demo:visitor,function-calling,,,exec-001
2027-05-01T10:07:01.000Z,2027-05-01T10:07:01.001Z,wanderlust.quote,wanderlust-api,succeeded,,demo:visitor,function-calling,,,exec-002
2027-05-01T10:07:02.000Z,2027-05-01T10:07:02.001Z,wanderlust.book,wanderlust-api,succeeded,,demo:visitor,function-calling,wanderlust-book-principals,,exec-003
2027-05-01T10:07:03.000Z,2027-05-01T10:07:03.001Z,wanderlust.cancel,wanderlust-api,succeeded,,demo:visitor,function-calling,wanderlust-cancel-principals,,exec-004
2027-05-01T10:07:04.000Z,2027-05-01T10:07:04.001Z,wanderlust.pay,wanderlust-api,succeeded,,demo:visitor,function-calling,wanderlust-pay-principals,,exec-005
$ archstone apply examples/showcase/manifest --exposureexit 0
stdout, 106 lines: show
archstone apply examples/showcase/manifest
company Wanderlust Agency (synthetic) (wanderlust)
providers wanderlust-api
declared 13 capabilities
loaded 13 capability docs, 13 bindings
✓ tourism.search-classic [read] → wanderlust-api
✓ tourism.search [read] → wanderlust-api
✓ wanderlust.availability [read] → wanderlust-api
✓ wanderlust.book [write] → wanderlust-api
✓ wanderlust.cancel [irreversible] → wanderlust-api
✓ wanderlust.neighbourhood [read] → wanderlust-api
✓ wanderlust.pay [irreversible] → wanderlust-api
✓ wanderlust.quote [write] → wanderlust-api
✓ wanderlust.room-status [read] → wanderlust-api
✓ wanderlust.search [read] → wanderlust-api
✓ wanderlust.stay-details [read] → wanderlust-api
✓ wanderlust.stay-page [read] → wanderlust-api
✓ wanderlust.stay-photos [read] → wanderlust-api
policies 4 policy document(s)
✓ wanderlust-availability-rate → capability wanderlust.availability
✓ wanderlust-book-principals → capability wanderlust.book
✓ wanderlust-cancel-principals → capability wanderlust.cancel
✓ wanderlust-pay-principals → capability wanderlust.pay
✓ shapes valid
semantic 0 error(s), 2 warning(s)
⚠ capability 'wanderlust.cancel' is irreversible and declares policies:[human-approval], which this version does not enforce: no approval mechanism exists. An agent can run it without anyone approving. Put the approval step in the provider, or do not serve this capability to an agent unattended.
⚠ capability 'wanderlust.pay' is irreversible and declares policies:[human-approval], which this version does not enforce: no approval mechanism exists. An agent can run it without anyone approving. Put the approval step in the provider, or do not serve this capability to an agent unattended.
registry IR v0 — 13 capabilities, 13 invocable (bound)
exposure what a model sends, is shown, and never sees — names and types only
totals 13 declared — 11 exposed, 1 unlisted, 1 not exposed
tourism.search [read] exposed
receives budget (money, optional), dates (date-range), destination (location), preferences (preference-set, optional), travelers (party)
exposes stays[].location (location), stays[].name (text), stays[].pricePerNight (quantity), stays[].rating (quantity, optional), totalMatches (quantity, extract)
withholds $.stays[].boardType (string), $.stays[].commission (number), $.stays[].freeCancellationUntil (string), $.stays[].id (string), $.stays[].net (number), $.stays[].roomDescription (string)
as observed in fixtures/tourism.search.golden.json (sha256:f5475f…)
tourism.search-classic [read] not exposed (retired)
receives budget (money, optional), dates (date-range), destination (location), travelers (party)
exposes stays[].location (location), stays[].name (text), stays[].pricePerNight (quantity), stays[].rating (quantity, optional)
withholds unknown — no recorded contract
wanderlust.availability [read] exposed
receives date (date), propertyId (identifier)
exposes availability.date (date), availability.pricePerNight (quantity), availability.propertyId (identifier), availability.roomsFree (quantity)
withholds unknown — no recorded contract
wanderlust.book [write] exposed
receives dates (date-range), guestName (string), quoteId (identifier), stayId (identifier), travelers (party)
exposes booking.bookingId (identifier), booking.dates (date-range), booking.payBy (datetime), booking.paymentQuote (identifier), booking.petFee (money), booking.pets (quantity), booking.status (enum), booking.stayId (identifier), booking.total (money)
withholds unknown — no recorded contract
wanderlust.cancel [irreversible] exposed
receives bookingId (identifier)
exposes cancellation.bookingId (identifier), cancellation.cancelledAt (datetime), cancellation.refund (money), cancellation.status (enum)
withholds unknown — no recorded contract
wanderlust.neighbourhood [read] unlisted (experimental) — callable by id, not advertised
receives area (text)
exposes neighbourhood.area (text), neighbourhood.summary (text), neighbourhood.walkability (quantity, optional)
withholds $.localTips (array), $.localTips[] (string)
as observed in fixtures/wanderlust.neighbourhood.golden.json (sha256:fa7430…)
wanderlust.pay [irreversible] exposed
receives amount (money), bookingId (identifier), paymentQuote (identifier)
exposes payment.amount (money), payment.bookingId (identifier), payment.paidAt (datetime), payment.paymentId (identifier), payment.status (enum)
withholds unknown — no recorded contract
wanderlust.quote [write] exposed
receives dates (date-range), pets (quantity, optional), stayId (identifier), travelers (party)
exposes quote.dates (date-range), quote.expiresAt (datetime), quote.nights (quantity), quote.petFee (money), quote.pets (quantity), quote.quoteId (identifier), quote.stayId (identifier), quote.total (money)
withholds unknown — no recorded contract
wanderlust.room-status [read] exposed
receives date (date), propertyId (identifier)
exposes rooms[].code (text), rooms[].message (text, optional), rooms[].pricePerNight (quantity), rooms[].room (text), rooms[].status (enum)
withholds $.date (string), $.propertyId (string)
as observed in fixtures/wanderlust.room-status.golden.json (sha256:d00727…)
wanderlust.search [read] exposed
receives budget (money, optional), dates (date-range), destination (location), preferences (preference-set, optional), travelers (party)
exposes stays[].id (identifier), stays[].location (location), stays[].name (text), stays[].petPolicy (text, optional), stays[].pricePerNight (quantity), stays[].rating (quantity, optional), totalMatches (quantity, extract)
withholds $.stays[].commission (number), $.stays[].currency (string), $.stays[].description_html (string), $.stays[].hostContact (object), $.stays[].hostContact.name (string), $.stays[].hostContact.phone (string), $.stays[].margin (number), $.stays[].net (number)
as observed in fixtures/wanderlust.search.golden.json (sha256:243302…)
wanderlust.stay-details [read] exposed
receives stayId (identifier)
exposes stay.location (location), stay.name (text), stay.petPolicy (text, optional), stay.pricePerNight (quantity), stay.rating (quantity, optional), stay.rooms (wanderlust.Room[])
withholds $.commission (number), $.currency (string), $.description_html (string), $.guests (array), $.guests[] (object), $.guests[].email (string), $.guests[].name (string), $.guests[].passport (string), $.guests[].phone (string), $.id (string), $.margin (number), $.net (number)
as observed in fixtures/wanderlust.stay-details.golden.json (sha256:1ed3fe…)
wanderlust.stay-page [read] exposed
receives stayId (identifier)
exposes pages[].name (text), pages[].url (web-page, optional)
withholds unknown — no recorded contract
wanderlust.stay-photos [read] exposed
receives stayId (identifier)
exposes gallery.photos (image[])
withholds unknown — no recorded contract
→ run 'archstone serve examples/showcase/manifest' to expose 13 tool(s) to an AI agent over MCP
What the recorder checked
- `build` exits 0 (the IR the embedded run loads)
- search ran (status ok)
- quote ran (status ok)
- book ran (status ok)
- cancel (irreversible) ran (status ok)
- pay (irreversible) ran (status ok)
- the trail holds one record per execution
- `doctor` exits 0: it reports health, it found no error
- `doctor` reports a summary of what it checked (13 capabilities, 0 errors)
- `doctor` names wanderlust.cancel as irreversible and its approval token as unenforced
- `doctor` names wanderlust.pay as irreversible and its approval token as unenforced
- `audit` exits 0
- `audit` counts the five executions
- `audit` lists the irreversible actions that ran: wanderlust.cancel and wanderlust.pay
- `apply --exposure` lists the exposed fields (exit 0)
What Archstone kept out, or did not do
- the backend received zero requests while these commands ran
- no command attempted any outbound connection (socket, DNS or fetch)
The test behind this
- recorded-s19.test.ts#L10 — recorded S-19: audit and doctor, zero outbound requests
S-20Start from the description of an existing API
archstone init reads an OpenAPI document and proposes one candidate per operation (16 here). It never defaults an effect: --non-interactive without a decisions file refuses and writes nothing. With a decisions file (the person's answers, shown below) it emits only the three confirmed capabilities; the DELETE operation was proposed and declined, and the draft mentions no passport, phone, email, margin, commission or raw HTML. It publishes nothing and makes no outbound connection.
The person's decisions
{
"version": "0",
"company": {
"id": "wanderlust",
"name": "Wanderlust Agency (synthetic)",
"description": "An invented travel agency; this draft is what `archstone init` proposes from its OpenAPI document."
},
"provider": "wanderlust-api",
"baseUrlEnvVar": "SHOWCASE_API_URL",
"decisions": [
{
"operation": "POST /v1/stays/search",
"keep": true,
"capabilityId": "wanderlust.search",
"effect": "read"
},
{
"operation": "GET /v1/stays/{stayId}",
"keep": true,
"capabilityId": "wanderlust.stay-details",
"effect": "read"
},
{
"operation": "GET /v1/availability",
"keep": true,
"capabilityId": "wanderlust.availability",
"effect": "read"
},
{
"operation": "DELETE /v1/guests/{name}/bookings",
"keep": false,
"note": "A delete function exists in the agency's system. A person declined it; nothing offers it to an AI."
}
]
}
Recording
Recorded with the core's recorder, CLI stamp 0.32.0 (the version when the content last changed), with a fixed clock (2027-05-01); machine-specific text is rewritten to placeholders (<refused>, <draft>). The file is vendored verbatim from the pinned commit: transcripts/s-20.json.
$ archstone init examples/showcase/api/wanderlust.openapi.yaml --out <draft> --decisions examples/showcase/record/s20-decisions.json --non-interactiveexit 0
stdout, 47 lines: show
archstone init — examples/showcase/api/wanderlust.openapi.yaml (openapi adapter)
Wrote 7 file(s) to <draft>, and the shipped compiler compiled them.
Candidates: 16 proposed, 3 emitted, 1 not emitted.
✓ wanderlust.search [read] POST /v1/stays/search (no resource — untyped output)
✓ wanderlust.stay-details [read] GET /v1/stays/{stayId} (no resource — untyped output)
✓ wanderlust.availability [read] GET /v1/availability (no resource — untyped output)
Declined by you (1) — most operations in a spec are not capabilities:
- DELETE /v1/guests/{name}/bookings — A delete function exists in the agency's system. A person declined it; nothing offers it to an AI.
What did not survive the trip — read these before committing:
no-response-shape [operation] (3) — no usable response shape — emitted without a resource, output or response mapping
- wanderlust.search
- wanderlust.stay-details
- wanderlust.availability
failures-not-emitted [operation] (1) — error responses carry no derivable business meaning — no `failures:` emitted
- GET /v1/stays/{stayId}: error responses declared: 404
contract-not-recorded [operation] (3) — no probe ran — no fixture, no contract
- wanderlust.search
- wanderlust.stay-details
- wanderlust.availability
semantic-type-degraded [field] (5) — semantic type fell back to string/text
- POST /v1/stays/search: POST /v1/stays/search/requestBody/content/application/json/schema/properties/destination: a bare `type: string` implies no richer type
- POST /v1/stays/search: #/components/schemas/Money/properties/currency: a bare `type: string` implies no richer type
- POST /v1/stays/search: POST /v1/stays/search/requestBody/content/application/json/schema/properties/preferences/items: a bare `type: string` implies no richer type
- GET /v1/stays/{stayId}: GET /v1/stays/{stayId}/parameters/0: a bare `type: string` implies no richer type
- GET /v1/availability: GET /v1/availability/parameters/0: a bare `type: string` implies no richer type
input-property-omitted [field] (3) — an optional input the connector cannot carry was left out — the agent cannot set it
- POST /v1/stays/search#dates: object
- POST /v1/stays/search#travelers: object
- POST /v1/stays/search#budget: object
identity-ref-not-inferred [field] (2) — emitted as `identifier`; this may be a `ref:` to a resource — only you know
- wanderlust.stay-details#stayId: if this identifies a resource another capability returns, change `type: identifier` to `ref: <Resource>`
- wanderlust.availability#propertyId: if this identifies a resource another capability returns, change `type: identifier` to `ref: <Resource>`
What no tool could infer for you, and what to check by hand:
- Every `description:` is the source's own words. An agent reads them to decide
whether to call you. They are the highest-leverage thing to rewrite.
- Every `response.map` line carries the field it came from and a real example.
A mapping can be structurally right and semantically wrong; only you can see that.
- An input typed `identifier` may really be a `ref:` to a resource another
capability returns. No source construct says so — only you know.
Report also written to <draft>/INIT-REPORT.md
$ archstone init examples/showcase/api/wanderlust.openapi.yaml --out <refused> --non-interactiveexit 2
archstone init: --non-interactive requires --decisions <file>. `init` never defaults an `effect`.
What the recorder checked
- `init` exits 0
- it proposed 16 candidates, one per operation in the document
- it emitted exactly the 3 confirmed capabilities
- `init --non-interactive` without --decisions refuses (non-zero): it never defaults an effect
- the draft has a capability file for each confirmed capability and no other
- the DELETE operation is in the document, so it was proposed, and the person declined it
- it wrote only inside --out: capabilities.yaml, the capability and binding files, and its report
- the draft compiles with the shipped compiler (`apply` exits 0)
What Archstone kept out, or did not do
- and it wrote nothing: the output directory does not exist
- the draft contains no delete action: no DELETE method, no capability or path that deletes
- the draft mentions no 'passport'
- the draft mentions no 'phone'
- the draft mentions no 'email'
- the draft mentions no 'margin'
- the draft mentions no 'commission'
- the draft mentions no 'description_html'
- no capability was drafted with a response mapping or a recorded contract (no probe ran)
- nothing was published: `init` made no outbound connection of any kind
The test behind this
- recorded-s20.test.ts#L13 — recorded S-20: no delete action in the draft; the DELETE was proposed and declined
S-21The embedded SDK: the same tools, no MCP server, no AI app
@archstone/agent loads the compiled IR with fromIR, hands the same capabilities to three vendors' tool-calling formats with tools(), and runs them with execute() through the same projection the MCP server uses. The script below is the whole example; it exits 1 and names what failed if any of its own expectations does not hold, and the page's check runs it against the published packages on every change.
Recording
Recorded with the core's recorder, CLI stamp 0.32.0 (the version when the content last changed), with a fixed clock (2027-05-01); machine-specific text is rewritten to placeholders (<api>, <work>). The file is vendored verbatim from the pinned commit: transcripts/s-21.json.
$ archstone build examples/showcase/manifest --out <work>/ir.jsonexit 0
archstone build examples/showcase/manifest → <work>/ir.json (13 tool(s))
$ node examples/showcase/sdk/embedded.mjs --ir <work>/ir.json --api <api>exit 0
stdout, 52 lines: show
{
"vendorShapes": {
"anthropic": {
"tools": 11,
"envelopeKeys": [
"description",
"input_schema",
"name"
]
},
"openai-chat": {
"tools": 11,
"envelopeKeys": [
"function",
"type"
]
},
"gemini": {
"tools": 11,
"envelopeKeys": [
"description",
"name",
"parameters"
]
}
},
"toolNames": [
"tourism_search",
"wanderlust_availability",
"wanderlust_book",
"wanderlust_cancel",
"wanderlust_pay",
"wanderlust_quote",
"wanderlust_room-status",
"wanderlust_search",
"wanderlust_stay-details",
"wanderlust_stay-page",
"wanderlust_stay-photos"
],
"stayDetails": {
"status": "ok",
"topLevelKeys": [
"stay"
],
"forbiddenKeysFound": []
},
"deleteRequest": {
"status": "error",
"error": "unknown capability: wanderlust.delete-guest-bookings"
},
"failures": []
}
What the recorder checked
- `build` exits 0 and writes the IR the SDK loads
- the embedded script exits 0 (it exits 1 when any of its own expectations fails)
- the same tools are offered in three vendor shapes (anthropic, openai-chat, gemini)
- the three envelopes differ from each other, the tool list does not
What Archstone kept out, or did not do
- S-02 holds: stay-details ran (ok) and returned none of margin, passport, phone, description_html, net, commission or the guest list
- S-14 holds: no tool deletes, and asking for one is refused as an unknown capability
- the refused request never reached the backend: the only request it saw was the stay lookup
Run it yourself
The script needs only the compiled IR and a base URL for the synthetic API. The public Worker serves that API on its own origin (https://demo.archstone.dev/v1/...), so a Node process can run against it directly; use pnpm --filter archstone-showcase api and --api http://localhost:8788 to run against your own copy.
git clone https://github.com/NousVigil/archstone.git && cd archstone && git checkout 6ff4bac6902190b3318ecb01f966a398d6a172a2
pnpm install && pnpm build
pnpm exec tsx packages/cli/src/index.ts build examples/showcase/manifest --out /tmp/ir.json
node examples/showcase/sdk/embedded.mjs --ir /tmp/ir.json --api https://demo.archstone.dev
The page's check also runs this exact script, copied verbatim from the pinned commit, with the published @archstone/cli and @archstone/agent (0.33.0) against the vendored synthetic API, and requires exit 0 and an empty failures list (11 tools).
// The embedded SDK, end to end, against the Showcase: no MCP server process, no AI app.
//
// archstone build examples/showcase/manifest --out ir.json
// SHOWCASE_API_URL=http://localhost:8788 node examples/showcase/sdk/embedded.mjs --ir ir.json
//
// It loads the compiled IR with `fromIR`, hands the SAME capabilities to three vendors' tool-calling
// formats with `tools()`, and runs two of them with `execute()`:
//
// S-02 the stay-details tool returns the declared fields and none of what the backend also sends
// (margin, passport, phone, raw HTML, the guest list);
// S-14 there is no tool that deletes, and asking `execute()` for one is refused before any request.
//
// It prints one JSON report on stdout, and exits 1 (naming what failed on stderr) if any of that does
// not hold. `tools()` is a lowering of the IR and `execute()` a call through the same projection the
// MCP server uses; neither makes the synthetic backend safe, they only decline to forward what the
// manifest does not name.
import { readFileSync } from "node:fs";
import { fromIR } from "@archstone/agent";
const argv = process.argv.slice(2);
const flag = (name) => {
const i = argv.indexOf(name);
return i >= 0 ? argv[i + 1] : undefined;
};
const irPath = flag("--ir");
if (!irPath) {
console.error("usage: node embedded.mjs --ir <ir.json> [--api <url>] (SHOWCASE_API_URL is used when --api is absent)");
process.exit(2);
}
const api = flag("--api") ?? process.env.SHOWCASE_API_URL;
const sdk = fromIR(JSON.parse(readFileSync(irPath, "utf8")));
const failures = [];
const expect = (claim, ok) => {
if (!ok) failures.push(claim);
};
// --- three vendor shapes, one set of tools
const nameOf = {
anthropic: (t) => t.name,
"openai-chat": (t) => t.function.name,
gemini: (t) => t.name,
};
const vendorShapes = {};
const names = {};
for (const format of Object.keys(nameOf)) {
const defs = sdk.tools(format);
names[format] = defs.map(nameOf[format]).sort();
vendorShapes[format] = { tools: defs.length, envelopeKeys: Object.keys(defs[0]).sort() };
}
const toolNames = names.anthropic;
expect("the three vendor shapes expose the same tools", Object.values(names).every((n) => JSON.stringify(n) === JSON.stringify(toolNames)));
expect("every shape lists at least ten tools", toolNames.length >= 10);
expect("no shape lists a tool that deletes", !toolNames.some((n) => /delete|remove|erase/i.test(n)));
expect("the shapes differ in envelope, not in content", new Set(Object.values(vendorShapes).map((v) => v.envelopeKeys.join())).size === 3);
// --- S-02: withholding still holds
const FORBIDDEN = ["margin", "passport", "phone", "description_html", "net", "commission", "guests"];
const keysOf = (value, out = new Set()) => {
if (Array.isArray(value)) value.forEach((v) => keysOf(v, out));
else if (value && typeof value === "object") {
for (const [k, v] of Object.entries(value)) {
out.add(k);
keysOf(v, out);
}
}
return out;
};
const stay = await sdk.execute("wanderlust.stay-details", { stayId: "ws-1001" }, { env: { SHOWCASE_API_URL: api } });
expect("stay-details returns status ok", stay.status === "ok");
const seen = keysOf(stay.data);
expect("stay-details returns the declared fields (stay.name, stay.rooms)", seen.has("name") && seen.has("rooms"));
const leaked = FORBIDDEN.filter((k) => seen.has(k));
expect(`stay-details returns none of ${FORBIDDEN.join(", ")} at any depth`, leaked.length === 0);
// --- S-14: the refusal still holds
const gone = await sdk.execute("wanderlust.delete-guest-bookings", { name: "Ana Pop" }, { env: { SHOWCASE_API_URL: api } });
expect("asking for a delete capability is refused as unknown", gone.status === "error" && /unknown capability/.test(gone.error ?? ""));
const report = {
vendorShapes,
toolNames,
stayDetails: { status: stay.status, topLevelKeys: Object.keys(stay.data ?? {}), forbiddenKeysFound: leaked },
deleteRequest: { status: gone.status, error: gone.error },
failures,
};
console.log(JSON.stringify(report, null, 2));
if (failures.length > 0) {
console.error(`embedded.mjs: ${failures.length} expectation(s) failed:\n - ${failures.join("\n - ")}`);
process.exit(1);
}
The test behind this
- recorded-s21.test.ts#L17 — recorded S-21: three vendor shapes; S-02 and S-14 still hold on the SDK path
Planned
S-22A declared HTML response: designed, not built
There is no HTML output type today, and nothing runs behind this anchor. The agency's description_html is not named by any resource, so the raw description is not sent at all (S-02 shows it dropped). A declared string field cannot express "this is markup", and nothing would check the images and links inside it.
Nothing runs behind this anchor. It is shown, not hidden, so the gap is visible.
What is planned
The design is ratified (ADR-0021, from RFC-0010); the build is deferred until a real provider's API returns HTML, and none of it exists yet. The rule: a declared html output whose every image and link satisfies the binding's declared origins is passed through unmodified; otherwise the whole value is withheld through the existing withheld path, which names the field and gives a closed reason category, never the offending URL. It is never cleaned, stripped, rewritten or sanitised. A provider marked trusted gets no exception (the ADR-0008 rule). A parse error, a <form>, a <script>, an inline style, or a relative link withholds the value too, and the runtime never fetches any URL inside the HTML. When a provider needs it, this scenario becomes executable with a clean-pass and a withheld pair.
description_html is absent from every result and from every advertised schema (S-02).The test behind this
- scenario-json.test.ts#L47 — the scenario table keeps this row locked and linked to its issue
Connect your own client
The Worker is a remote MCP server: https://demo.archstone.dev/mcp, Streamable HTTP, stateless, POST only (a GET is 405), JSON responses. The only credential is an optional Authorization: Bearer <key> header. There are two public demo keys; they are published here, in the core repository and in the Worker's README on purpose, and unlock nothing but invented data.
| Header | Principal the Worker asserts | Effect |
|---|---|---|
Authorization: Bearer demo-public-key-visitor-0000 | demo:visitor | accepted by the agency, allowed to book, pay and cancel (S-06, S-08, S-09) |
Authorization: Bearer demo-public-key-blocked-0000 | demo:blocked | accepted by the agency, denied by the policies of book, pay and cancel: principal_denied (S-07) |
| any other bearer | none | a credential is present, no principal (S-07) |
a header that is not Bearer ... | cannot be resolved | refused, fail closed: policy_unevaluatable (S-07) |
| no header | none | every keyless tool works; a tool that needs a credential is refused: authenticated_no_credential (S-06) |
Claude on the web, in the desktop app and on mobile can add the URL alone as a custom connector (Settings → Connectors → Add custom connector); that path has no field for a header, so it reaches every keyless tool. To send a key, use one of the blocks below. They carry the Worker's URL and header; the surrounding structure follows each client's documented configuration format, which this page does not test.
claude mcp add --transport http wanderlust https://demo.archstone.dev/mcp \
--header "Authorization: Bearer demo-public-key-visitor-0000"
{
"mcpServers": {
"wanderlust": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://demo.archstone.dev/mcp",
"--header",
"Authorization:${AUTH_HEADER}"
],
"env": {
"AUTH_HEADER": "Bearer demo-public-key-visitor-0000"
}
}
}
}
{
"mcpServers": {
"wanderlust": {
"url": "https://demo.archstone.dev/mcp",
"headers": {
"Authorization": "Bearer demo-public-key-visitor-0000"
}
}
}
}
{
"servers": {
"wanderlust": {
"type": "http",
"url": "https://demo.archstone.dev/mcp",
"headers": {
"Authorization": "Bearer demo-public-key-visitor-0000"
}
}
}
}
curl -s -X POST https://demo.archstone.dev/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-H 'authorization: Bearer demo-public-key-visitor-0000' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
The four limits, in technical terms
The Showcase demonstrates a compiler's guarantees on data we invented. It does not show that any real backend is safe because Archstone sits in front of it.
1. Effects are hints the client may honour or ignore
A capability's effect (read, write, irreversible) is lowered to MCP tool annotations (readOnlyHint, destructiveHint, idempotentHint). Archstone tells the client which actions cannot be undone; whether the AI asks a person first is the client's decision. Archstone enforces none of it and takes no credit for a confirmation a client chose to ask. What the Worker advertises right now (captured tools/list):
| Tool | Annotations |
|---|---|
tourism_search | readOnlyHint: true |
wanderlust_availability | readOnlyHint: true |
wanderlust_book | destructiveHint: false |
wanderlust_cancel | destructiveHint: true, idempotentHint: false |
wanderlust_pay | destructiveHint: true, idempotentHint: false |
wanderlust_quote | destructiveHint: false |
wanderlust_room-status | readOnlyHint: true |
wanderlust_search | readOnlyHint: true |
wanderlust_stay-details | readOnlyHint: true |
wanderlust_stay-page | readOnlyHint: true |
wanderlust_stay-photos | readOnlyHint: true |
2. Approval before an irreversible action is declared, not enforced
policies: [human-approval] on wanderlust.cancel and wanderlust.pay is parsed and lowered, nothing more. No approval mechanism exists, apply and doctor say so, and the lint irreversible-unenforced-policy is a warning that changes no exit code. Runtime enforcement is planned: S-10, NousVigil/archstone#165.
3. A refusal from the agency reaches the AI as a bare status
When the agency itself refuses a call that Archstone let through (an expired payment quote, for example), the result is an error whose text is backend returned 422: the status code, without the reason the agency's body declares, and no _meta. onError maps error rows inside a 2xx answer; a non-2xx answer never reaches the response mapping. Planned: NousVigil/archstone#175. Refusals by Archstone itself (a policy, the input contract, a lifecycle, a rate limit) are different: they carry a reason code in _meta.
4. The demo key is public and everything it reaches is invented
The two keys are defined in credentials.mjs and published on purpose. The principal is asserted by the Worker from a two-entry table lookup; Archstone treats it as an opaque string and verifies nothing. The agency, its hotels, guests, passport numbers (DEMO-PASS-nnnnnn), phone numbers, e-mail addresses (reserved .example domain) and payments are all invented. The backend over-exposes on purpose. Archstone stops fields the manifest does not name from being forwarded and refuses calls the manifest says to refuse; it does not fix what a backend stores, returns elsewhere, or does when asked directly. Do not copy this key pattern for a real credential.
What does not work yet, said plainly
Locked and partial items are shown, never hidden.
| Where | Item | What happens today | Status |
|---|---|---|---|
| S-10 | Runtime-enforced human approval | Not implemented. human-approval is declared, not enforced; the cancel and pay calls above run at once. | Locked, #165 |
| S-22 | HTML response type | Design ratified (ADR-0021), build deferred until a provider needs it. The raw description is not sent at all. | Locked, #166 |
| S-17 | Strict verify | A gained field is a yellow reading: named, exit 0. Only a lost field, a changed type or a missing required value exit 1. | Planned, #178 |
| S-17 | adopt on a manifest with two resources named Stay | Resolved a resource by bare name. Fixed in core 0.32.0; the recording predates the fix and still runs it on wanderlust.stay-details. | Fixed, #177 |
| S-03, S-04 | Outcome of a withheld field over MCP | A successful MCP result carries no outcome in _meta; the withheld path is a text note. status: degraded and withheld exist on the embedded SDK's execute() result. | Planned, #174 |
| S-09 | A refusal from the agency | Surfaces only backend returned 422. An expired or foreign payment quote is the agency's rule, not Archstone's, and no _meta is attached. (A payment with no quote at all is refused earlier, by the input contract, with a reason.) | Planned, #175 |
| S-13 | A failing backend | Reported as an error with its status (backend returned 503), never as degraded: a non-2xx answer never reaches the response mapping. | Planned, #175 |
| S-11 | Rate limit on the live Worker | Approximate: a fixed window in memory per isolate; another isolate or a restart starts a new count. The Node test asserts the exact fourth refusal on a fake clock. | By design of a demo prop |
Reference
The tool a client receives
What tools/list advertises for wanderlust_stay-details, captured from the Worker: the input schema, the output schema (the declared fields only, nested to the same depth as the projection) and the annotations.
{
"name": "wanderlust_stay-details",
"description": "Tell me more about one stay: its rooms and what each room offers.",
"inputSchema": {
"type": "object",
"properties": {
"stayId": {
"description": "The stay's id: the `id` field of a row returned by wanderlust_search (it looks like `ws-1001`), never the stay's name. To find a stay the user names, first call wanderlust_search with that name as `destination`.",
"type": "string"
}
},
"required": [
"stayId"
],
"additionalProperties": false
},
"outputSchema": {
"type": "object",
"properties": {
"stay": {
"type": "object",
"properties": {
"name": {
"description": "The property's display name.",
"type": "string"
},
"location": {
"description": "Where the stay is.",
"type": "string"
},
"pricePerNight": {
"description": "Nightly rate from, in euro.",
"type": "number"
},
"rating": {
"description": "Guest review score, when available.",
"type": "number"
},
"petPolicy": {
"description": "The stay's pet policy, e.g. cats welcome with a nightly fee, or no pets.",
"type": "string"
},
"rooms": {
"description": "The room types on offer.",
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"description": "Room type.",
"type": "string"
},
"sleeps": {
"description": "How many people it sleeps.",
"type": "number"
},
"pricePerNight": {
"description": "Nightly rate for this room in euro.",
"type": "number"
},
"amenities": {
"description": "What the room offers.",
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"description": "What the amenity is.",
"type": "string"
},
"fee": {
"description": "Fee in euro; absent when it is free.",
"type": "number"
}
},
"required": [
"name"
]
}
}
},
"required": [
"name",
"sleeps",
"pricePerNight",
"amenities"
]
}
}
},
"required": [
"name",
"location",
"pricePerNight",
"rooms"
]
}
},
"required": [
"stay"
]
},
"annotations": {
"readOnlyHint": true
}
}
Validating what a model extracted
The same resources that project a backend's answer also validate a model's own extraction: extractor("wanderlust.Stay", "anthropic").validate(doc) from @archstone/agent accepts a document with the declared fields, and rejects one with a missing required field, a wrong type, or a field nobody declared (an invented margin). Re-captured against the published @archstone/agent 0.33.0 on every check.
| Case | Input | Result |
|---|---|---|
ok | {"id":"ws-1001","name":"Casa Alfama","location":"Lisbon","pricePerNight":118,"rating":4.6} | {"status":"degraded","data":{"id":"ws-1001","name":"Casa Alfama","location":"Lisbon","pricePerNight":118,"rating":4.6},"degraded":["petPolicy"]} |
noRating | {"id":"ws-1001","name":"Casa Alfama","location":"Lisbon","pricePerNight":118} | {"status":"degraded","data":{"id":"ws-1001","name":"Casa Alfama","location":"Lisbon","pricePerNight":118},"degraded":["petPolicy","rating"]} |
missingRequired | {"id":"ws-1001","name":"Casa Alfama","location":"Lisbon","rating":4.6} | {"status":"violation","missing":["pricePerNight"],"degraded":["petPolicy"]} |
invented | {"id":"ws-1001","name":"Casa Alfama","location":"Lisbon","pricePerNight":118,"rating":4.6,"margin":40} | {"status":"degraded","data":{"id":"ws-1001","name":"Casa Alfama","location":"Lisbon","pricePerNight":118,"rating":4.6},"degraded":["petPolicy"],"undeclared":["margin"]} |
wrongType | {"id":"ws-1001","name":"Casa Alfama","location":"Lisbon","pricePerNight":"118","rating":4.6} | {"status":"violation","invalid":["pricePerNight: expected number"],"degraded":["petPolicy"]} |
Lifecycle across the manifest
A capability can be experimental, beta, stable (the default), deprecated or retired. The non-default ones in the Wanderlust manifest, read from the vendored files, and whether the Worker lists them in tools/list (captured):
| Capability | Lifecycle | Listed to a client |
|---|---|---|
tourism.search | deprecated | yes |
tourism.search-classic | retired | no |
wanderlust.availability | beta | yes |
wanderlust.neighbourhood | experimental | no |
The experimental one, wanderlust.neighbourhood (tool name wanderlust_neighbourhood), is the one to notice: it is not in the tool list a client receives, yet a client that calls it by name is answered (the core's test for this is manifests.test.ts#L93; this page does not call it live). The core's README says an experimental capability is unlisted but still callable by name, a deprecated one answers with a phasing-out note in its tool description (S-12), and a retired one is refused (S-12).
Run it yourself, on Wanderlust
The earlier demo gave the real command under each of its sections. They are the same commands, pointed at the vendored Wanderlust manifest in a checkout of the core at the pinned commit. The manifest needs the CLI from 0.32.0 or later (the image type). What none of this shows you is your own API: that is what Verify my API is for.
npm install -g @archstone/cli
archstone apply examples/showcase/manifest/ # validate + compile
SHOWCASE_API_URL=https://demo.archstone.dev archstone serve examples/showcase/manifest/ # MCP over stdio, against the public synthetic API
archstone serve --http examples/showcase/manifest/ --token my-bearer-token # MCP over HTTP
archstone build examples/showcase/manifest/ --out archstone.ir.json # portable IR
archstone init examples/showcase/api/wanderlust.openapi.yaml --out draft --decisions examples/showcase/record/s20-decisions.json --non-interactive
archstone apply examples/showcase/manifest/ --exposure # add --json for CI
archstone adopt examples/showcase/manifest/ # declare a field, deliberately (needs a person)
SHOWCASE_API_URL=https://demo.archstone.dev archstone verify examples/showcase/manifest/ # green / yellow / red per binding
archstone diff manifest-v1/ manifest-v2/ # what changed for an agent; exit 1 if breaking
archstone doctor examples/showcase/manifest/ # pre-production checklist, offline
archstone audit audit.log --phase denied --format csv
import { readFileSync } from "node:fs";
import { fromIR } from "@archstone/agent";
const archstone = fromIR(JSON.parse(readFileSync("archstone.ir.json", "utf-8")));
const myTools = archstone.tools("anthropic"); // or openai-chat | openai-responses | gemini | json-schema
const result = await archstone.execute("wanderlust.search", input, { env });
// validating what a model extracted (the other direction):
const stay = archstone.extractor("wanderlust.Stay", "anthropic");
stay.structuredOutput; // the closed schema, in your provider's envelope
const checked = stay.validate(modelOutput); // ok | degraded | violation, undeclared keys named
// governance: pass identity, a counter and an audit sink on every call
await archstone.execute("wanderlust.book", input, {
caller: { principal: "user:alice" }, // asserted by YOUR authentication
rateLimitCounter: counter,
auditSink: jsonLinesAuditSink(createWriteStream("audit.log", { flags: "a" })),
});
// MCP over HTTP from the same instance, on your own router:
import { mcpHandler } from "@archstone/agent/mcp";
const handler = mcpHandler(archstone, { bearerToken: process.env.ARCHSTONE_TOKEN });
Providers and proof
A binding says how a capability reaches a backend. The compiler, the tool and the declaration are the same whichever provider sits behind them.
REST is the provider every live scenario above uses: an HTTP method and path on a baseUrl that comes from your environment (${SHOWCASE_API_URL}), a JSONPath map from the response onto the resource, and optional caller credentials (${caller.accessToken}, S-06). archstone init writes it from an OpenAPI document (S-20).
SQL (Postgres): @archstone/provider-sql binds a capability directly to Postgres, read-only (SELECT). Tenant isolation is enforced by the database (row-level security, with a runtime role that is not the table owner), and a connection that is over-privileged is refused at startup. It is Node-only, so it is not part of the edge or browser build and not part of the live Worker; S-15 is a recording. Its design record is still marked a draft.
Proof: a business running on it. ArtVinci, a custom-framing business in Romania, answers customer questions today through capabilities compiled by Archstone, with real catalog and prices computed live by its own backend. It does not run archstone serve: it loads the compiled artifact with the embedded SDK (S-21). The captured conversation, with its real question and real prices, is on the homepage; the full write-up is the case study.
Where the earlier demo's technical sections went
The earlier /demo simulated ten sections on a tourism sample. Their technical content now runs on the Wanderlust agency above, not on the old sample.
| Earlier section | Now |
|---|---|
Write the capability, get the tool (compile, optional and required fields, diff) | S-01, S-18, commands in run it yourself |
Start from an OpenAPI document (init) | S-20 |
| The model learns the shape before it calls (typed schemas) | the tool a client receives, S-02 |
A field you did not name never reaches a model (exposure, adopt) | S-02, S-16, S-17 |
| When the backend breaks the contract, the answer is an error (fail closed) | S-13 |
Your API changes; you hear about it first (verify, diff, doctor) | S-17, S-18, S-19 |
A field the model invents never reaches your system (extractor) | validating what a model extracted |
| Disclosed, refused, counted, recorded (effects, lifecycle, policy, rate limit, audit) | S-05, S-08, S-12, lifecycle across the manifest, S-06, S-07, S-11, S-19, the audit call in run it yourself |
| No server process: the embedded SDK | S-21 |
| Reach the backend you have (REST, SQL), and the proof | every live scenario (REST), S-15 (SQL), providers and proof |